@lowdefy/server-dev 0.0.0-experimental-20260714092003 → 0.0.0-experimental-20260714122106
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/client/feedback/FeedbackOverlay.jsx +26 -4
- package/lib/client/auth/Auth.jsx +3 -5
- package/lib/client/auth/AuthConfigured.jsx +115 -35
- package/lib/client/auth/AuthNotConfigured.js +22 -2
- package/lib/client/utils/usePageConfig.js +7 -0
- package/lib/docs/getBrowser.js +11 -0
- package/lib/server/auth/createSystemContext.js +68 -0
- package/lib/server/auth/getAuth.js +46 -0
- package/lib/server/auth/getHeadlessSession.js +39 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +11 -19
- package/lib/server/auth/{getAuthConfig.js → getStrategies.js} +12 -10
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/createLowdefyContext.js +50 -41
- package/lib/server/loadDynamicJsMap.js +47 -0
- package/lib/server/log/logRequest.js +1 -3
- package/manager/processes/startServer.mjs +0 -2
- package/manager/run.mjs +3 -0
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -2
- package/manager/watchers/serverArtifactWatcher.mjs +0 -2
- package/package.json +36 -34
- package/package.original.json +36 -34
- package/src/app.js +21 -14
- package/src/middleware/errorHandler.js +12 -1
- package/src/routes/auth.js +15 -10
- package/src/routes/docs/runRequest.js +2 -4
- package/src/routes/jitPage.js +21 -2
- package/{lib/server/auth/session.js → src/routes/user.js} +9 -18
- package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
- package/src/routes/apiPage.js +0 -37
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import React, { useEffect, useReducer, useRef, useState } from 'react';
|
|
18
|
+
import { createPortal } from 'react-dom';
|
|
18
19
|
|
|
19
20
|
import { blockAncestorChain, describeElement, nearestBlock } from './elementInspect.js';
|
|
20
21
|
import {
|
|
@@ -663,7 +664,16 @@ function FeedbackOverlay({ basePath, pageId, onClose }) {
|
|
|
663
664
|
} catch {
|
|
664
665
|
content = null;
|
|
665
666
|
}
|
|
666
|
-
|
|
667
|
+
// Portal to document.body so the overlay is a top-level sibling of the app
|
|
668
|
+
// root. Rendered inline it inherits the app root's stacking context, and an
|
|
669
|
+
// open antd modal (portalled to body, zIndex ~1000) then paints above the
|
|
670
|
+
// overlay regardless of its own zIndex — making the comment textarea
|
|
671
|
+
// unclickable. As a body sibling its zIndex competes in the root stacking
|
|
672
|
+
// context and wins.
|
|
673
|
+
if (typeof document === 'undefined' || !document.body) {
|
|
674
|
+
return content;
|
|
675
|
+
}
|
|
676
|
+
return createPortal(content, document.body);
|
|
667
677
|
}
|
|
668
678
|
|
|
669
679
|
// Pure render function kept outside the hook-bearing component body so a
|
|
@@ -776,7 +786,11 @@ function renderOverlayContent({
|
|
|
776
786
|
<div style={descriptorRow}>{state.draftShapes.length} shape(s) drawn</div>
|
|
777
787
|
)}
|
|
778
788
|
<div style={buttonRow}>
|
|
779
|
-
<button
|
|
789
|
+
<button
|
|
790
|
+
type="button"
|
|
791
|
+
style={secondaryButton}
|
|
792
|
+
onClick={() => dispatch({ type: 'DISCARD_DRAFT' })}
|
|
793
|
+
>
|
|
780
794
|
Cancel
|
|
781
795
|
</button>
|
|
782
796
|
<button type="button" style={primaryButton} onClick={handleSave}>
|
|
@@ -823,10 +837,18 @@ function renderOverlayContent({
|
|
|
823
837
|
</label>
|
|
824
838
|
{state.sendError && <div style={errorRow}>{state.sendError}</div>}
|
|
825
839
|
<div style={buttonRow}>
|
|
826
|
-
<button
|
|
840
|
+
<button
|
|
841
|
+
type="button"
|
|
842
|
+
style={dangerButton}
|
|
843
|
+
onClick={() => dispatch({ type: 'DISCARD_ALL' })}
|
|
844
|
+
>
|
|
827
845
|
Discard all
|
|
828
846
|
</button>
|
|
829
|
-
<button
|
|
847
|
+
<button
|
|
848
|
+
type="button"
|
|
849
|
+
style={secondaryButton}
|
|
850
|
+
onClick={() => dispatch({ type: 'ADD_ANOTHER' })}
|
|
851
|
+
>
|
|
830
852
|
Add another
|
|
831
853
|
</button>
|
|
832
854
|
<button
|
package/lib/client/auth/Auth.jsx
CHANGED
|
@@ -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,15 +23,15 @@ 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
|
|
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,
|
|
31
|
+
function Auth({ children, user }) {
|
|
34
32
|
if (authConfig.configured === true) {
|
|
35
33
|
return (
|
|
36
|
-
<AuthConfigured
|
|
34
|
+
<AuthConfigured serverUser={user} authConfig={authConfig}>
|
|
37
35
|
{(auth) => children(auth)}
|
|
38
36
|
</AuthConfigured>
|
|
39
37
|
);
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/* eslint-disable react/jsx-props-no-spreading */
|
|
18
|
-
|
|
19
17
|
import React, { useEffect, useRef } from 'react';
|
|
18
|
+
import { createAuthClient } from 'better-auth/react';
|
|
20
19
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from '
|
|
20
|
+
adminClient,
|
|
21
|
+
genericOAuthClient,
|
|
22
|
+
magicLinkClient,
|
|
23
|
+
organizationClient,
|
|
24
|
+
phoneNumberClient,
|
|
25
|
+
twoFactorClient,
|
|
26
|
+
} from 'better-auth/client/plugins';
|
|
27
|
+
import { passkeyClient } from '@better-auth/passkey/client';
|
|
28
28
|
|
|
29
29
|
import { serializer } from '@lowdefy/helpers';
|
|
30
30
|
|
|
@@ -32,42 +32,122 @@ import rawLowdefyConfig from '../../../build/config.json';
|
|
|
32
32
|
|
|
33
33
|
const lowdefyConfig = serializer.deserialize(rawLowdefyConfig);
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const authClient = createAuthClient({
|
|
36
|
+
baseURL: `${window.location.origin}${lowdefyConfig.basePath ?? ''}/api/auth`,
|
|
37
|
+
plugins: [
|
|
38
|
+
adminClient(),
|
|
39
|
+
genericOAuthClient(),
|
|
40
|
+
magicLinkClient(),
|
|
41
|
+
organizationClient(),
|
|
42
|
+
passkeyClient(),
|
|
43
|
+
phoneNumberClient(),
|
|
44
|
+
twoFactorClient(),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
// The server resolves the caller per request and embeds it in the page
|
|
49
|
+
// config, so the first render never flashes unauthenticated. The BetterAuth
|
|
50
|
+
// client store takes over once its session fetch settles.
|
|
51
|
+
//
|
|
52
|
+
// Roles and merged attributes resolve server-side from the active member row
|
|
53
|
+
// - the base session carries neither. The last server-resolved caller is
|
|
54
|
+
// kept in a ref: while the session user is unchanged its roles and
|
|
55
|
+
// attributes stay authoritative, and UpdateSession refreshes the ref from
|
|
56
|
+
// /api/user after a change (e.g. SetActiveOrganization).
|
|
57
|
+
function Session({ children, reloadSuppressedRef, serverUser }) {
|
|
58
|
+
const { data: session, isPending } = authClient.useSession();
|
|
59
|
+
const resolvedUserRef = useRef(serverUser);
|
|
60
|
+
const wasAuthenticated = useRef(Boolean(serverUser));
|
|
45
61
|
|
|
46
62
|
useEffect(() => {
|
|
47
|
-
if (
|
|
63
|
+
if (session) {
|
|
64
|
+
wasAuthenticated.current = true;
|
|
65
|
+
}
|
|
66
|
+
// Reload after sign-out (or session revocation) so the server can apply
|
|
67
|
+
// the page auth fork - a protected page redirects to the login page.
|
|
68
|
+
// A logout with a callbackUrl suppresses the reload - its own navigation
|
|
69
|
+
// would otherwise race this one.
|
|
70
|
+
if (wasAuthenticated.current && !isPending && !session && !reloadSuppressedRef.current) {
|
|
48
71
|
window.location.reload();
|
|
49
72
|
}
|
|
50
|
-
}, [
|
|
73
|
+
}, [session, isPending]);
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
if (isPending) {
|
|
76
|
+
return children(resolvedUserRef.current, resolvedUserRef);
|
|
77
|
+
}
|
|
78
|
+
if (!session?.user) {
|
|
79
|
+
return children(null, resolvedUserRef);
|
|
56
80
|
}
|
|
57
|
-
|
|
81
|
+
const resolved = resolvedUserRef.current;
|
|
82
|
+
const user =
|
|
83
|
+
resolved && resolved.id === session.user.id
|
|
84
|
+
? { ...session.user, roles: resolved.roles, attributes: resolved.attributes }
|
|
85
|
+
: { roles: [], ...session.user };
|
|
86
|
+
return children(user, resolvedUserRef);
|
|
58
87
|
}
|
|
59
88
|
|
|
60
|
-
function AuthConfigured({ authConfig, children,
|
|
61
|
-
const
|
|
89
|
+
function AuthConfigured({ authConfig, children, serverUser }) {
|
|
90
|
+
const reloadSuppressedRef = useRef(false);
|
|
91
|
+
const auth = {
|
|
92
|
+
authConfig,
|
|
93
|
+
getSession: ({ disableCookieCache } = {}) =>
|
|
94
|
+
authClient.getSession(disableCookieCache ? { query: { disableCookieCache: true } } : {}),
|
|
95
|
+
// The server-resolved caller - roles from the active member row and the
|
|
96
|
+
// merged attributes bag - for re-syncing after session changes.
|
|
97
|
+
getResolvedUser: async () => {
|
|
98
|
+
const response = await fetch(`${lowdefyConfig.basePath ?? ''}/api/user`, {
|
|
99
|
+
credentials: 'same-origin',
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
// Throw instead of parsing an error page - a silent { user: undefined }
|
|
103
|
+
// would read as a client-side logout in updateSession.
|
|
104
|
+
throw new Error(`Failed to fetch the resolved user (HTTP ${response.status}).`);
|
|
105
|
+
}
|
|
106
|
+
return response.json();
|
|
107
|
+
},
|
|
108
|
+
suppressSignOutReload: () => {
|
|
109
|
+
reloadSuppressedRef.current = true;
|
|
110
|
+
},
|
|
111
|
+
acceptInvitation: (params) => authClient.organization.acceptInvitation(params),
|
|
112
|
+
// addPasskey runs the WebAuthn browser ceremony itself - options fetch,
|
|
113
|
+
// authenticator prompt, verification.
|
|
114
|
+
addPasskey: (params) => authClient.passkey.addPasskey(params),
|
|
115
|
+
changePassword: (params) => authClient.changePassword(params),
|
|
116
|
+
deletePasskey: (params) => authClient.passkey.deletePasskey(params),
|
|
117
|
+
impersonateUser: (params) => authClient.admin.impersonateUser(params),
|
|
118
|
+
phoneNumberRequestPasswordReset: (params) =>
|
|
119
|
+
authClient.phoneNumber.requestPasswordReset(params),
|
|
120
|
+
phoneNumberResetPassword: (params) => authClient.phoneNumber.resetPassword(params),
|
|
121
|
+
phoneNumberSendOtp: (params) => authClient.phoneNumber.sendOtp(params),
|
|
122
|
+
phoneNumberVerify: (params) => authClient.phoneNumber.verify(params),
|
|
123
|
+
requestPasswordReset: (params) => authClient.requestPasswordReset(params),
|
|
124
|
+
resetPassword: (params) => authClient.resetPassword(params),
|
|
125
|
+
revokeOtherSessions: () => authClient.revokeOtherSessions(),
|
|
126
|
+
sendVerificationEmail: (params) => authClient.sendVerificationEmail(params),
|
|
127
|
+
setActiveOrganization: (params) => authClient.organization.setActive(params),
|
|
128
|
+
signInEmail: (params) => authClient.signIn.email(params),
|
|
129
|
+
signInMagicLink: (params) => authClient.signIn.magicLink(params),
|
|
130
|
+
signInOauth2: (params) => authClient.signIn.oauth2(params),
|
|
131
|
+
signInPhoneNumber: (params) => authClient.signIn.phoneNumber(params),
|
|
132
|
+
signInSocial: (params) => authClient.signIn.social(params),
|
|
133
|
+
signOut: () => authClient.signOut(),
|
|
134
|
+
signUpEmail: (params) => authClient.signUp.email(params),
|
|
135
|
+
stopImpersonating: () => authClient.admin.stopImpersonating(),
|
|
136
|
+
twoFactorDisable: (params) => authClient.twoFactor.disable(params),
|
|
137
|
+
twoFactorEnable: (params) => authClient.twoFactor.enable(params),
|
|
138
|
+
twoFactorVerifyBackupCode: (params) => authClient.twoFactor.verifyBackupCode(params),
|
|
139
|
+
twoFactorVerifyTotp: (params) => authClient.twoFactor.verifyTotp(params),
|
|
140
|
+
};
|
|
62
141
|
return (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
142
|
+
<Session reloadSuppressedRef={reloadSuppressedRef} serverUser={serverUser}>
|
|
143
|
+
{(user, resolvedUserRef) => {
|
|
144
|
+
auth.user = user;
|
|
145
|
+
auth.updateResolvedUser = (resolved) => {
|
|
146
|
+
resolvedUserRef.current = resolved;
|
|
147
|
+
};
|
|
148
|
+
return children(auth);
|
|
149
|
+
}}
|
|
150
|
+
</Session>
|
|
71
151
|
);
|
|
72
152
|
}
|
|
73
153
|
|
|
@@ -13,7 +13,6 @@
|
|
|
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 */
|
|
17
16
|
|
|
18
17
|
function authNotConfigured() {
|
|
19
18
|
throw new Error('Auth not configured.');
|
|
@@ -22,9 +21,30 @@ function authNotConfigured() {
|
|
|
22
21
|
function AuthNotConfigured({ authConfig, children }) {
|
|
23
22
|
const auth = {
|
|
24
23
|
authConfig,
|
|
24
|
+
user: null,
|
|
25
|
+
acceptInvitation: authNotConfigured,
|
|
26
|
+
addPasskey: authNotConfigured,
|
|
27
|
+
changePassword: authNotConfigured,
|
|
28
|
+
deletePasskey: authNotConfigured,
|
|
29
|
+
getResolvedUser: authNotConfigured,
|
|
25
30
|
getSession: authNotConfigured,
|
|
26
|
-
|
|
31
|
+
impersonateUser: authNotConfigured,
|
|
32
|
+
requestPasswordReset: authNotConfigured,
|
|
33
|
+
resetPassword: authNotConfigured,
|
|
34
|
+
revokeOtherSessions: authNotConfigured,
|
|
35
|
+
sendVerificationEmail: authNotConfigured,
|
|
36
|
+
setActiveOrganization: authNotConfigured,
|
|
37
|
+
signInEmail: authNotConfigured,
|
|
38
|
+
signInMagicLink: authNotConfigured,
|
|
39
|
+
signInOauth2: authNotConfigured,
|
|
40
|
+
signInSocial: authNotConfigured,
|
|
27
41
|
signOut: authNotConfigured,
|
|
42
|
+
signUpEmail: authNotConfigured,
|
|
43
|
+
stopImpersonating: authNotConfigured,
|
|
44
|
+
twoFactorDisable: authNotConfigured,
|
|
45
|
+
twoFactorEnable: authNotConfigured,
|
|
46
|
+
twoFactorVerifyBackupCode: authNotConfigured,
|
|
47
|
+
twoFactorVerifyTotp: authNotConfigured,
|
|
28
48
|
};
|
|
29
49
|
|
|
30
50
|
return children(auth);
|
|
@@ -54,6 +54,13 @@ async function fetchPageConfig(url) {
|
|
|
54
54
|
if (res.status === 404) {
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
+
if (res.status === 401) {
|
|
58
|
+
// Logged-out navigation to a protected page - full load to the login
|
|
59
|
+
// page so it can return here after sign-in.
|
|
60
|
+
const { redirect } = await res.json();
|
|
61
|
+
window.location.assign(redirect ?? '/404');
|
|
62
|
+
return new Promise(() => {});
|
|
63
|
+
}
|
|
57
64
|
const data = await res.json();
|
|
58
65
|
if (data?.buildError) {
|
|
59
66
|
return data;
|
package/lib/docs/getBrowser.js
CHANGED
|
@@ -18,6 +18,7 @@ import { chromium } from 'playwright-core';
|
|
|
18
18
|
import { type } from '@lowdefy/helpers';
|
|
19
19
|
|
|
20
20
|
import lowdefyConfig from '../build/config.js';
|
|
21
|
+
import { HEADLESS_USER_COOKIE, headlessUser } from '../server/auth/headlessUser.js';
|
|
21
22
|
|
|
22
23
|
// playwright-core does not bundle a browser (unlike @playwright/test) — it
|
|
23
24
|
// only drives one that is already installed. `channel: 'chrome'` picks up a
|
|
@@ -73,6 +74,16 @@ function buildPageUrl({ origin, pageId }) {
|
|
|
73
74
|
async function openPage({ browser, origin, pageId, width = 1280, height = 800, timeout = 15000 }) {
|
|
74
75
|
const url = buildPageUrl({ origin, pageId });
|
|
75
76
|
const context = await browser.newContext({ viewport: { width, height } });
|
|
77
|
+
// Inject an authenticated user so auth-protected pages don't 404 for the
|
|
78
|
+
// cookieless headless context. Mirrors the e2e user-cookie pattern; scoped to
|
|
79
|
+
// `origin` so it rides along on the same-origin /api/* fetches.
|
|
80
|
+
await context.addCookies([
|
|
81
|
+
{
|
|
82
|
+
name: HEADLESS_USER_COOKIE,
|
|
83
|
+
value: Buffer.from(JSON.stringify(headlessUser)).toString('base64'),
|
|
84
|
+
url: origin,
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
76
87
|
const page = await context.newPage();
|
|
77
88
|
try {
|
|
78
89
|
await page.goto(url, { waitUntil: 'networkidle', timeout });
|
|
@@ -0,0 +1,68 @@
|
|
|
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 path from 'node:path';
|
|
18
|
+
import { createSystemContext as buildSystemContext } from '@lowdefy/api';
|
|
19
|
+
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
20
|
+
import { v4 as uuid } from 'uuid';
|
|
21
|
+
|
|
22
|
+
import agents from '../../../build/plugins/agents.js';
|
|
23
|
+
import appMeta from '../../build/appMeta.js';
|
|
24
|
+
import config from '../../build/config.js';
|
|
25
|
+
import connections from '../../../build/plugins/connections.js';
|
|
26
|
+
import createHandleError from '../log/createHandleError.js';
|
|
27
|
+
import createLogger from '../log/createLogger.js';
|
|
28
|
+
import fileCache from '../fileCache.js';
|
|
29
|
+
import i18nConfig from '../../build/i18n.js';
|
|
30
|
+
import loadDynamicJsMap from '../loadDynamicJsMap.js';
|
|
31
|
+
import operators from '../../../build/plugins/operators/server.js';
|
|
32
|
+
import steps from '../../../build/plugins/steps.js';
|
|
33
|
+
import websockets from '../../../build/plugins/websockets.js';
|
|
34
|
+
|
|
35
|
+
const secrets = getSecretsFromEnv();
|
|
36
|
+
|
|
37
|
+
// Builds a fresh system context per auth hook fire - the trusted internal
|
|
38
|
+
// caller the BetterAuth engine uses to invoke hook endpoints. All fields are
|
|
39
|
+
// startup singletons except the per-fire rid, logger, handleError, the
|
|
40
|
+
// dynamic jsMap re-read (JIT rebuilds rewrite serverJsMap.js), and auth -
|
|
41
|
+
// auth is threaded in by the caller (createHookDispatch) rather than
|
|
42
|
+
// imported directly, since it comes from the same BetterAuth construction
|
|
43
|
+
// that this factory is a dependency of (see createHookDispatch.js).
|
|
44
|
+
function createSystemContext({ auth } = {}) {
|
|
45
|
+
const rid = uuid();
|
|
46
|
+
const buildDirectory = path.join(process.cwd(), 'build');
|
|
47
|
+
return buildSystemContext({
|
|
48
|
+
rid,
|
|
49
|
+
agents,
|
|
50
|
+
appMeta,
|
|
51
|
+
auth,
|
|
52
|
+
buildDirectory,
|
|
53
|
+
config,
|
|
54
|
+
configDirectory: process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd(),
|
|
55
|
+
connections,
|
|
56
|
+
createHandleError,
|
|
57
|
+
fileCache,
|
|
58
|
+
i18n: i18nConfig,
|
|
59
|
+
jsMap: loadDynamicJsMap(buildDirectory),
|
|
60
|
+
logger: createLogger({ rid }),
|
|
61
|
+
operators,
|
|
62
|
+
secrets,
|
|
63
|
+
steps,
|
|
64
|
+
websockets,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default createSystemContext;
|
|
@@ -0,0 +1,46 @@
|
|
|
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 { getBetterAuth } from '@lowdefy/api';
|
|
18
|
+
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
19
|
+
|
|
20
|
+
import adapters from '../../../build/plugins/auth/adapters.js';
|
|
21
|
+
import appMeta from '../../build/appMeta.js';
|
|
22
|
+
import authJson from '../../build/auth.js';
|
|
23
|
+
import createSystemContext from './createSystemContext.js';
|
|
24
|
+
import lowdefyConfig from '../../build/config.js';
|
|
25
|
+
import providers from '../../../build/plugins/auth/providers.js';
|
|
26
|
+
|
|
27
|
+
// Returns the BetterAuth instance (memoized in @lowdefy/api), or null when
|
|
28
|
+
// auth is not configured. dev: true relaxes cookies for plain-http localhost
|
|
29
|
+
// and derives a per-app cookie prefix so parallel dev apps do not collide.
|
|
30
|
+
function getAuth({ logger }) {
|
|
31
|
+
if (authJson.configured !== true) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return getBetterAuth({
|
|
35
|
+
appMeta,
|
|
36
|
+
authJson,
|
|
37
|
+
config: lowdefyConfig,
|
|
38
|
+
createSystemContext,
|
|
39
|
+
dev: true,
|
|
40
|
+
logger,
|
|
41
|
+
plugins: { adapters, providers },
|
|
42
|
+
secrets: getSecretsFromEnv(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default getAuth;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { HEADLESS_USER_COOKIE } from './headlessUser.js';
|
|
18
|
+
|
|
19
|
+
// Mirrors the e2e user-injection pattern (server-e2e/lib/server/auth/session.js):
|
|
20
|
+
// the headless renderer sets a base64-JSON user cookie on its own browser
|
|
21
|
+
// context, so its /api/* fetches carry a session while the developer's real
|
|
22
|
+
// browser (no cookie) is unaffected.
|
|
23
|
+
function getHeadlessSession(c) {
|
|
24
|
+
const cookieHeader = c.req.header('cookie') ?? '';
|
|
25
|
+
const match = cookieHeader.match(new RegExp(`${HEADLESS_USER_COOKIE}=([^;]+)`));
|
|
26
|
+
if (!match) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
|
|
32
|
+
const user = JSON.parse(decoded);
|
|
33
|
+
return { user };
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default getHeadlessSession;
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { createSessionCallback } from '@lowdefy/api';
|
|
18
17
|
import { serializer } from '@lowdefy/helpers';
|
|
19
18
|
|
|
20
19
|
import authJson from '../../build/auth.js';
|
|
21
|
-
import callbacks from '../../../build/plugins/auth/callbacks.js';
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
// dev.mockUser is a pre-resolved caller that substitutes for the whole
|
|
22
|
+
// resolveAuthentication step - no auth engine, no session lookup, no
|
|
23
|
+
// membership wall. Its roles are authoritative. LOWDEFY_DEV_USER takes
|
|
24
|
+
// priority over auth.dev.mockUser from lowdefy.yaml.
|
|
25
|
+
function getMockUser() {
|
|
24
26
|
const mockUserJson = process.env.LOWDEFY_DEV_USER;
|
|
25
27
|
let mockUser;
|
|
26
28
|
|
|
@@ -35,7 +37,7 @@ async function getMockSession() {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
if (!mockUser) {
|
|
38
|
-
return
|
|
40
|
+
return null;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
// Deserialize to restore arrays from ~arr markers and remove other build markers
|
|
@@ -48,20 +50,10 @@ async function getMockSession() {
|
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Transform mock user through session callback (mock user acts as token)
|
|
58
|
-
const session = await sessionCallback({
|
|
59
|
-
session: { user: {} },
|
|
60
|
-
token: mockUser,
|
|
61
|
-
user: mockUser,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return session;
|
|
53
|
+
if (!mockUser.roles) {
|
|
54
|
+
mockUser.roles = [];
|
|
55
|
+
}
|
|
56
|
+
return mockUser;
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
export default
|
|
59
|
+
export default getMockUser;
|
|
@@ -14,25 +14,27 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { getAuthStrategies } from '@lowdefy/api';
|
|
18
18
|
import { getSecretsFromEnv } from '@lowdefy/node-utils';
|
|
19
19
|
|
|
20
|
-
import adapters from '../../../build/plugins/auth/adapters.js';
|
|
21
20
|
import appMeta from '../../build/appMeta.js';
|
|
22
21
|
import authJson from '../../build/auth.js';
|
|
23
|
-
import
|
|
24
|
-
import events from '../../../build/plugins/auth/events.js';
|
|
25
|
-
import providers from '../../../build/plugins/auth/providers.js';
|
|
22
|
+
import strategyTypes from '../../../build/plugins/auth/strategies.js';
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// Returns the API auth strategy verifiers (memoized in @lowdefy/api), or an
|
|
25
|
+
// empty list when auth is not configured - resolveAuthentication tries them
|
|
26
|
+
// in config order when no session resolves.
|
|
27
|
+
function getStrategies({ logger }) {
|
|
28
|
+
if (authJson.configured !== true) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return getAuthStrategies({
|
|
29
32
|
appMeta,
|
|
30
33
|
authJson,
|
|
31
|
-
dev: true,
|
|
32
34
|
logger,
|
|
33
|
-
plugins: {
|
|
35
|
+
plugins: { strategies: strategyTypes },
|
|
34
36
|
secrets: getSecretsFromEnv(),
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
export default
|
|
40
|
+
export default getStrategies;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Shared between the headless renderer (getBrowser.js, which sets the cookie on
|
|
18
|
+
// its browser context) and the dev getSession (getHeadlessSession.js, which
|
|
19
|
+
// decodes it) so the cookie name and injected user shape live in one place.
|
|
20
|
+
const HEADLESS_USER_COOKIE = 'lowdefy_headless_user';
|
|
21
|
+
|
|
22
|
+
const headlessUser = {
|
|
23
|
+
sub: 'lowdefy-headless',
|
|
24
|
+
id: 'lowdefy-headless',
|
|
25
|
+
name: 'Lowdefy Headless',
|
|
26
|
+
roles: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { HEADLESS_USER_COOKIE, headlessUser };
|