@lowdefy/server 5.6.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{pages/_app.js → client/App.jsx} +26 -34
- package/client/Page.jsx +122 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/main.jsx +39 -0
- package/client/shouldReloadForBuild.js +48 -0
- package/client/shouldReloadForBuild.test.mjs +75 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +10 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +21 -10
- package/lib/client/sentry/captureSentryError.js +1 -1
- package/lib/client/sentry/initSentryClient.js +1 -1
- package/lib/client/sentry/setSentryUser.js +1 -1
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/{pages/api/auth/[...nextauth].js → lib/server/auth/session.js} +9 -15
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/log/createHandleError.js +10 -5
- package/lib/server/log/createHandleError.test.mjs +85 -0
- package/lib/server/log/createLogger.js +28 -1
- package/lib/server/log/createLogger.test.mjs +102 -0
- package/lib/server/log/logRequest.js +47 -45
- package/lib/server/log/maskCredentialKeys.js +37 -0
- package/lib/server/log/projectErrorForLog.js +55 -0
- package/lib/server/log/redactUrlQuery.js +75 -0
- package/lib/server/log/redactUrlQuery.test.mjs +73 -0
- package/lib/server/{auth/getServerSession.js → log/serializeErrorForLog.js} +5 -8
- package/lib/server/log/serializeErrorForLog.test.mjs +325 -0
- package/lib/server/scrubSecrets.js +21 -0
- package/lib/server/sentry/captureSentryError.js +13 -1
- package/lib/server/sentry/captureSentryError.test.mjs +104 -0
- package/lib/server/sentry/initSentry.js +17 -4
- package/lib/server/sentry/initSentry.test.mjs +237 -0
- package/lib/server/sentry/scrubEvent.js +29 -0
- package/lib/server/sentry/scrubEvent.test.mjs +88 -0
- package/lib/server/sentry/setSentryUser.js +1 -1
- package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
- package/package.json +54 -42
- package/package.original.json +56 -44
- package/src/app.js +140 -0
- package/src/html/getAssets.js +45 -0
- package/src/html/renderPage.js +80 -0
- package/src/html/template.js +95 -0
- package/src/index.js +95 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +138 -0
- package/src/middleware/apiContext.test.mjs +118 -0
- package/src/middleware/errorHandler.js +54 -0
- package/src/middleware/sentry.js +33 -0
- package/{pages/api/agent/[...path].js → src/routes/agent.js} +20 -42
- package/src/routes/apiPage.js +40 -0
- package/src/routes/apiPage.test.mjs +59 -0
- package/src/routes/auth.js +38 -0
- package/{pages/api/client-error.js → src/routes/clientError.js} +14 -16
- package/src/routes/cron.js +44 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +46 -0
- package/vite.config.js +61 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/Page.js +0 -56
- package/lib/server/apiWrapper.js +0 -87
- package/lib/server/serverSidePropsWrapper.js +0 -66
- package/next.config.js +0 -34
- package/pages/404.js +0 -49
- package/pages/[[...pageId]].js +0 -85
- package/pages/_document.js +0 -108
- package/pages/api/endpoints/[...endpointId].js +0 -32
- package/pages/api/usage.js +0 -67
- /package/{postcss.config.js → postcss.config.cjs} +0 -0
|
@@ -14,13 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
// CSS layer order — MUST be the first CSS import. Next.js treats this as critical
|
|
18
|
-
// CSS that loads before hydration, locking the cascade priority (antd > base/preflight)
|
|
19
|
-
// before antd's StyleProvider injects @layer antd {} at runtime.
|
|
20
|
-
import '../build/layer-order.css';
|
|
21
|
-
|
|
22
17
|
import React, { useCallback, useRef } from 'react';
|
|
23
|
-
import dynamic from 'next/dynamic';
|
|
24
18
|
|
|
25
19
|
import { ErrorBoundary } from '@lowdefy/block-utils';
|
|
26
20
|
import { useDarkMode, useLocale } from '@lowdefy/client';
|
|
@@ -28,23 +22,20 @@ import { StyleProvider } from '@ant-design/cssinjs';
|
|
|
28
22
|
import { App as AntdApp, theme as antdTheme } from 'antd';
|
|
29
23
|
import { XProvider } from '@ant-design/x';
|
|
30
24
|
|
|
25
|
+
import { serializer } from '@lowdefy/helpers';
|
|
26
|
+
|
|
31
27
|
import antdLocaleLoaders from '../build/i18n/antdLocales.js';
|
|
32
28
|
import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
|
|
33
29
|
import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
|
|
34
|
-
import
|
|
30
|
+
import rawLoggerConfig from '../build/logger.json';
|
|
31
|
+
import Auth from '../lib/client/auth/Auth.jsx';
|
|
35
32
|
import createLogUsage from '../lib/client/createLogUsage.js';
|
|
36
33
|
import initSentryClient from '../lib/client/sentry/initSentryClient.js';
|
|
37
|
-
import loggerConfig from '../lib/build/logger.js';
|
|
38
34
|
import setSentryUser from '../lib/client/sentry/setSentryUser.js';
|
|
35
|
+
import Page from './Page.jsx';
|
|
39
36
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Initialize Sentry client once on module load
|
|
44
|
-
initSentryClient({
|
|
45
|
-
sentryDsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
46
|
-
sentryConfig: loggerConfig.sentry,
|
|
47
|
-
});
|
|
37
|
+
// Deserialize to restore arrays (e.g. sentry.userFields) from ~arr markers.
|
|
38
|
+
const loggerConfig = serializer.deserialize(rawLoggerConfig);
|
|
48
39
|
|
|
49
40
|
function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
50
41
|
const { token } = antdTheme.useToken();
|
|
@@ -55,7 +46,16 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
|
|
|
55
46
|
return children;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
function App({
|
|
49
|
+
function App({ config }) {
|
|
50
|
+
const { rootConfig, session } = config;
|
|
51
|
+
|
|
52
|
+
// Sentry DSN arrives at runtime via the embedded config (the server reads
|
|
53
|
+
// SENTRY_DSN per process) — no build-time env inlining.
|
|
54
|
+
initSentryClient({
|
|
55
|
+
sentryDsn: config.sentryDsn,
|
|
56
|
+
sentryConfig: loggerConfig.sentry,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
59
|
const usageDataRef = useRef({});
|
|
60
60
|
const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
|
|
61
61
|
if (rootConfig?.theme) {
|
|
@@ -67,7 +67,11 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
67
67
|
configDarkMode: lowdefyRef.current.theme?.darkMode,
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
const {
|
|
70
|
+
const {
|
|
71
|
+
active: activeLocale,
|
|
72
|
+
antdLocale,
|
|
73
|
+
antdXLocale,
|
|
74
|
+
} = useLocale({
|
|
71
75
|
i18n: rootConfig?.i18n,
|
|
72
76
|
antdLocaleLoaders,
|
|
73
77
|
antdXLocaleLoaders,
|
|
@@ -98,9 +102,8 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
98
102
|
// gives X components (Bubble, Sender, Conversations, ...) their built-in strings
|
|
99
103
|
// alongside antd's. antd X ships only en_US + zh_CN; other locales fall back
|
|
100
104
|
// to en_US for X-native strings.
|
|
101
|
-
const mergedLocale =
|
|
102
|
-
? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) }
|
|
103
|
-
: undefined;
|
|
105
|
+
const mergedLocale =
|
|
106
|
+
antdLocale || antdXLocale ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) } : undefined;
|
|
104
107
|
|
|
105
108
|
return (
|
|
106
109
|
<StyleProvider layer>
|
|
@@ -131,14 +134,7 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
131
134
|
user: auth.session,
|
|
132
135
|
sentryConfig: loggerConfig.sentry,
|
|
133
136
|
});
|
|
134
|
-
return
|
|
135
|
-
<Component
|
|
136
|
-
auth={auth}
|
|
137
|
-
lowdefy={lowdefyRef.current}
|
|
138
|
-
rootConfig={rootConfig}
|
|
139
|
-
pageConfig={pageConfig}
|
|
140
|
-
/>
|
|
141
|
-
);
|
|
137
|
+
return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
|
|
142
138
|
}}
|
|
143
139
|
</Auth>
|
|
144
140
|
</ErrorBoundary>
|
|
@@ -149,8 +145,4 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
|
|
|
149
145
|
);
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
|
|
153
|
-
ssr: false,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
export default DynamicApp;
|
|
148
|
+
export default App;
|
package/client/Page.jsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
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, { useEffect, useRef, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import Client from '@lowdefy/client';
|
|
20
|
+
import createRouter from '@lowdefy/client/adapters/createRouter.js';
|
|
21
|
+
import createLinkComponent from '@lowdefy/client/adapters/Link.js';
|
|
22
|
+
import { createUrl } from '@lowdefy/client/adapters/url.js';
|
|
23
|
+
import Head from '@lowdefy/client/adapters/Head.js';
|
|
24
|
+
|
|
25
|
+
import actions from '../build/plugins/actions.js';
|
|
26
|
+
import blockMetas from '../build/plugins/blockMetas.json';
|
|
27
|
+
import blocks from '../build/plugins/blocks.js';
|
|
28
|
+
import icons from '../build/plugins/icons.js';
|
|
29
|
+
import operators from '../build/plugins/operators/client.js';
|
|
30
|
+
import jsMap from '../build/plugins/operators/clientJsMap.js';
|
|
31
|
+
import appMeta from '../build/appMeta.json';
|
|
32
|
+
|
|
33
|
+
import shouldReloadForBuild from './shouldReloadForBuild.js';
|
|
34
|
+
|
|
35
|
+
// Replaces lib/client/Page.js. The first page renders from the config
|
|
36
|
+
// embedded in the HTML; SPA navigations fetch /api/page/* and swap pageConfig.
|
|
37
|
+
function Page({ auth, config, lowdefy }) {
|
|
38
|
+
const [pageConfig, setPageConfig] = useState(config.pageConfig);
|
|
39
|
+
|
|
40
|
+
const routerRef = useRef(null);
|
|
41
|
+
if (!routerRef.current) {
|
|
42
|
+
const router = createRouter({ basePath: config.basePath ?? '', window });
|
|
43
|
+
routerRef.current = {
|
|
44
|
+
router,
|
|
45
|
+
Link: createLinkComponent({ router }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const { router, Link } = routerRef.current;
|
|
49
|
+
|
|
50
|
+
// Temporary sequence guard for the superseded-navigation race: a slow
|
|
51
|
+
// response for an abandoned navigation must not paint over a newer one.
|
|
52
|
+
// Remove when the loader-based lifecycle replaces these fetch paths.
|
|
53
|
+
const latestNavRef = useRef(0);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const unsubscribe = router.subscribe(async ({ pageId }) => {
|
|
57
|
+
const token = ++latestNavRef.current;
|
|
58
|
+
const targetPageId = pageId ?? config.rootConfig.home.pageId;
|
|
59
|
+
try {
|
|
60
|
+
// Forward the current query string so server-side Dynamic block
|
|
61
|
+
// resolution sees the same urlQuery as an initial HTML load.
|
|
62
|
+
const res = await fetch(
|
|
63
|
+
`${router.basePath}/api/page/${targetPageId}${window.location.search}`
|
|
64
|
+
);
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
if (token !== latestNavRef.current) return;
|
|
67
|
+
if (targetPageId !== '404') {
|
|
68
|
+
router.replace({ pathname: '/404' });
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { buildId, pageConfig: nextPageConfig } = await res.json();
|
|
73
|
+
if (token !== latestNavRef.current) return;
|
|
74
|
+
if (
|
|
75
|
+
shouldReloadForBuild({ bundleBuildId: appMeta.buildId, serverBuildId: buildId, window })
|
|
76
|
+
) {
|
|
77
|
+
// The app was redeployed after this bundle loaded, so the config may
|
|
78
|
+
// reference _js functions and plugins this bundle does not carry.
|
|
79
|
+
// The router has already pushed the target URL, so a reload lands
|
|
80
|
+
// on the requested page with the current bundle.
|
|
81
|
+
window.location.reload();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setPageConfig(nextPageConfig);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// Network failure on SPA navigation — fall back to a full page load.
|
|
87
|
+
if (token !== latestNavRef.current) return;
|
|
88
|
+
window.location.assign(
|
|
89
|
+
createUrl({
|
|
90
|
+
basePath: router.basePath,
|
|
91
|
+
pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return unsubscribe;
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Client
|
|
101
|
+
auth={auth}
|
|
102
|
+
Components={{ Head, Link }}
|
|
103
|
+
config={{
|
|
104
|
+
pageConfig,
|
|
105
|
+
rootConfig: config.rootConfig,
|
|
106
|
+
}}
|
|
107
|
+
jsMap={jsMap}
|
|
108
|
+
lowdefy={lowdefy}
|
|
109
|
+
router={router}
|
|
110
|
+
types={{
|
|
111
|
+
actions,
|
|
112
|
+
blockMetas,
|
|
113
|
+
blocks,
|
|
114
|
+
icons,
|
|
115
|
+
operators,
|
|
116
|
+
}}
|
|
117
|
+
window={window}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default Page;
|
|
@@ -0,0 +1,61 @@
|
|
|
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 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
// Everything the client imports from build/ ships in the browser bundle. build/config.json is
|
|
22
|
+
// server-only: it carries every deployment environment's settings (other environments' urls,
|
|
23
|
+
// secret names, email filters). The client learns only the current environment's name and
|
|
24
|
+
// switched-off features, through appMeta. This allowlist fails the build of any new client import
|
|
25
|
+
// of a build artifact until it has been checked for what it exposes.
|
|
26
|
+
const allowed = new Set([
|
|
27
|
+
'auth.json',
|
|
28
|
+
'appMeta.json',
|
|
29
|
+
'logger.json',
|
|
30
|
+
'i18n/antdLocales.js',
|
|
31
|
+
'i18n/antdXLocales.js',
|
|
32
|
+
'i18n/dayjsLocales.js',
|
|
33
|
+
'plugins/actions.js',
|
|
34
|
+
'plugins/blockMetas.json',
|
|
35
|
+
'plugins/blocks.js',
|
|
36
|
+
'plugins/icons.js',
|
|
37
|
+
'plugins/operators/client.js',
|
|
38
|
+
'plugins/operators/clientJsMap.js',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
42
|
+
|
|
43
|
+
function listSourceFiles(dir) {
|
|
44
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
45
|
+
const full = path.join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) return listSourceFiles(full);
|
|
47
|
+
return /\.(js|jsx|mjs)$/.test(entry.name) && !entry.name.includes('.test.') ? [full] : [];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('client code imports only client-safe build artifacts', () => {
|
|
52
|
+
const imported = ['client', 'lib/client'].flatMap((dir) =>
|
|
53
|
+
listSourceFiles(path.join(root, dir)).flatMap((file) =>
|
|
54
|
+
[...fs.readFileSync(file, 'utf8').matchAll(/from '[^']*\/build\/([^']+)'/g)].map(
|
|
55
|
+
(match) => ({ file: path.relative(root, file), artifact: match[1] })
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
expect(imported.length).toBeGreaterThan(0);
|
|
60
|
+
expect(imported.filter(({ artifact }) => !allowed.has(artifact))).toEqual([]);
|
|
61
|
+
});
|
package/client/main.jsx
ADDED
|
@@ -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
|
+
// CSS layer order — MUST be the first CSS import. This locks the cascade
|
|
18
|
+
// priority (antd > base/preflight) before antd's StyleProvider injects
|
|
19
|
+
// @layer antd {} at runtime.
|
|
20
|
+
import '../build/layer-order.css';
|
|
21
|
+
|
|
22
|
+
import React from 'react';
|
|
23
|
+
import { createRoot } from 'react-dom/client';
|
|
24
|
+
|
|
25
|
+
import App from './App.jsx';
|
|
26
|
+
|
|
27
|
+
import '../build/globals.css';
|
|
28
|
+
|
|
29
|
+
const config = JSON.parse(document.getElementById('__LOWDEFY_CONFIG__').textContent);
|
|
30
|
+
const container = document.getElementById('root');
|
|
31
|
+
|
|
32
|
+
// Keep one React root across Vite HMR updates — re-running createRoot on a
|
|
33
|
+
// container that already has a root is an error.
|
|
34
|
+
const root = import.meta.hot?.data.root ?? createRoot(container);
|
|
35
|
+
if (import.meta.hot) {
|
|
36
|
+
import.meta.hot.data.root = root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
root.render(<App config={config} />);
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
// sessionStorage key holding the server build this tab last reloaded for.
|
|
20
|
+
const RELOADED_FOR_BUILD_KEY = 'lowdefy.reloadedForBuild';
|
|
21
|
+
|
|
22
|
+
// True when a page config fetched during SPA navigation came from a newer
|
|
23
|
+
// deploy than the bundle this tab is running, so the tab must do a full load
|
|
24
|
+
// to pick up the current bundle. Without this the new config can reference
|
|
25
|
+
// _js functions the old bundle never shipped, and the page throws mid-event.
|
|
26
|
+
//
|
|
27
|
+
// A tab reloads at most once per server build: if the freshly loaded bundle
|
|
28
|
+
// still disagrees with the server (a rolling deploy answering from mixed
|
|
29
|
+
// versions, a proxy caching old HTML), reloading again would loop forever.
|
|
30
|
+
// The old-config-on-old-bundle throw is the lesser failure in that case.
|
|
31
|
+
function shouldReloadForBuild({ bundleBuildId, serverBuildId, window }) {
|
|
32
|
+
if (type.isNone(serverBuildId) || serverBuildId === bundleBuildId) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
if (window.sessionStorage.getItem(RELOADED_FOR_BUILD_KEY) === serverBuildId) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
window.sessionStorage.setItem(RELOADED_FOR_BUILD_KEY, serverBuildId);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Browsers with storage disabled throw on sessionStorage access. Without
|
|
42
|
+
// the once-per-build record the loop guard cannot work, so do not reload.
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default shouldReloadForBuild;
|
|
@@ -0,0 +1,75 @@
|
|
|
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 shouldReloadForBuild from './shouldReloadForBuild.js';
|
|
18
|
+
|
|
19
|
+
function createWindow({ stored = {}, throws = false } = {}) {
|
|
20
|
+
return {
|
|
21
|
+
sessionStorage: {
|
|
22
|
+
getItem: (key) => {
|
|
23
|
+
if (throws) throw new Error('storage disabled');
|
|
24
|
+
return stored[key] ?? null;
|
|
25
|
+
},
|
|
26
|
+
setItem: (key, value) => {
|
|
27
|
+
if (throws) throw new Error('storage disabled');
|
|
28
|
+
stored[key] = value;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('shouldReloadForBuild is false when the server build matches the bundle', () => {
|
|
35
|
+
const window = createWindow();
|
|
36
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-1', window })).toBe(
|
|
37
|
+
false
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('shouldReloadForBuild is false when the server sends no build id', () => {
|
|
42
|
+
const window = createWindow();
|
|
43
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: undefined, window })).toBe(
|
|
44
|
+
false
|
|
45
|
+
);
|
|
46
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: null, window })).toBe(
|
|
47
|
+
false
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('shouldReloadForBuild is true once per newer server build, then false', () => {
|
|
52
|
+
const stored = {};
|
|
53
|
+
const window = createWindow({ stored });
|
|
54
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
|
|
55
|
+
true
|
|
56
|
+
);
|
|
57
|
+
expect(stored['lowdefy.reloadedForBuild']).toEqual('build-2');
|
|
58
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
|
|
59
|
+
false
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('shouldReloadForBuild reloads again when the server moves to a third build', () => {
|
|
64
|
+
const window = createWindow({ stored: { 'lowdefy.reloadedForBuild': 'build-2' } });
|
|
65
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-2', serverBuildId: 'build-3', window })).toBe(
|
|
66
|
+
true
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('shouldReloadForBuild is false when sessionStorage is unavailable', () => {
|
|
71
|
+
const window = createWindow({ throws: true });
|
|
72
|
+
expect(shouldReloadForBuild({ bundleBuildId: 'build-1', serverBuildId: 'build-2', window })).toBe(
|
|
73
|
+
false
|
|
74
|
+
);
|
|
75
|
+
});
|
package/lib/build/app.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/app.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/appMeta.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/appMeta.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/auth.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/auth.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/config.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/config.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/i18n.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/i18n.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/logger.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/logger.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
package/lib/build/theme.js
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
// Build artifacts are read from disk because the Hono server runs as
|
|
18
|
+
// unbundled Node.js ESM — JSON imports would need import attributes, and
|
|
19
|
+
// client code imports the build JSON directly through Vite instead.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
16
22
|
import { serializer } from '@lowdefy/helpers';
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/theme.json'), 'utf8'));
|
|
18
25
|
|
|
19
26
|
export default serializer.deserialize(raw);
|
|
@@ -17,10 +17,18 @@
|
|
|
17
17
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
18
18
|
|
|
19
19
|
import React from 'react';
|
|
20
|
-
import AuthConfigured from './AuthConfigured.
|
|
20
|
+
import AuthConfigured from './AuthConfigured.jsx';
|
|
21
21
|
import AuthNotConfigured from './AuthNotConfigured.js';
|
|
22
22
|
|
|
23
|
-
import
|
|
23
|
+
import { serializer } from '@lowdefy/helpers';
|
|
24
|
+
|
|
25
|
+
// Client code imports the build JSON directly — Vite handles JSON imports;
|
|
26
|
+
// the lib/build/*.js wrappers are server-only (they read from disk).
|
|
27
|
+
// Deserialize to restore arrays from their ~arr build markers (providers
|
|
28
|
+
// must be a real array for single-provider inference in createAuthMethods).
|
|
29
|
+
import rawAuthConfig from '../../../build/auth.json';
|
|
30
|
+
|
|
31
|
+
const authConfig = serializer.deserialize(rawAuthConfig);
|
|
24
32
|
|
|
25
33
|
function Auth({ children, session }) {
|
|
26
34
|
if (authConfig.configured === true) {
|
|
@@ -17,9 +17,25 @@
|
|
|
17
17
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
18
18
|
|
|
19
19
|
import React, { useEffect, useRef } from 'react';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
authConfigManager,
|
|
22
|
+
getSession,
|
|
23
|
+
SessionProvider,
|
|
24
|
+
signIn,
|
|
25
|
+
signOut,
|
|
26
|
+
useSession,
|
|
27
|
+
} from '@hono/auth-js/react';
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
// The app basePath, from Vite's BASE_URL (`${config.basePath}/`, set in
|
|
30
|
+
// vite.config.js). build/config.json is server-only - it carries every
|
|
31
|
+
// deployment environment's settings - so it is never imported into the client.
|
|
32
|
+
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
33
|
+
|
|
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
|
+
}
|
|
23
39
|
|
|
24
40
|
function Session({ children }) {
|
|
25
41
|
const wasAuthenticated = useRef(false);
|
|
@@ -32,9 +48,8 @@ function Session({ children }) {
|
|
|
32
48
|
}
|
|
33
49
|
}, [status]);
|
|
34
50
|
|
|
35
|
-
// If session is passed to SessionProvider from
|
|
36
|
-
// we won't have a loading state here.
|
|
37
|
-
// But 404 uses getStaticProps so we have this for 404.
|
|
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.
|
|
38
53
|
if (status === 'loading') {
|
|
39
54
|
return '';
|
|
40
55
|
}
|
|
@@ -43,12 +58,8 @@ function Session({ children }) {
|
|
|
43
58
|
|
|
44
59
|
function AuthConfigured({ authConfig, children, serverSession }) {
|
|
45
60
|
const auth = { authConfig, getSession, signIn, signOut };
|
|
46
|
-
let basePath = lowdefyConfig.basePath;
|
|
47
|
-
if (basePath) {
|
|
48
|
-
basePath = `${basePath}/api/auth`;
|
|
49
|
-
}
|
|
50
61
|
return (
|
|
51
|
-
<SessionProvider session={serverSession}
|
|
62
|
+
<SessionProvider session={serverSession}>
|
|
52
63
|
<Session>
|
|
53
64
|
{(session) => {
|
|
54
65
|
auth.session = session;
|