@lowdefy/api 5.5.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/createAuthorize.js +4 -1
- package/dist/context/resolveStrategyCaller.js +56 -0
- package/dist/index.js +16 -3
- package/dist/response/buildEndpointResult.js +33 -0
- package/dist/response/normalizeErrorSources.js +64 -0
- package/dist/response/omitErrorProps.js +51 -0
- package/dist/response/redactErrorResponse.js +30 -0
- package/dist/response/redactResponse.js +31 -0
- package/dist/routes/agent/authorizeAgent.js +39 -0
- package/dist/routes/agent/callAgent.js +7 -180
- package/dist/routes/agent/prepareAgent.js +173 -0
- package/dist/routes/auth/createLogger.js +2 -5
- package/dist/routes/auth/createPrefixedCookies.js +52 -0
- package/dist/routes/auth/{getNextAuthConfig.js → getAuthConfig.js} +27 -16
- package/dist/routes/auth/resolveCookies.js +37 -0
- package/dist/routes/auth/strategies/createAuthStrategies.js +77 -0
- package/dist/routes/auth/strategies/getAuthStrategies.js +31 -0
- package/dist/routes/endpoints/addStepResult.js +12 -2
- package/dist/routes/endpoints/authorizeApiEndpoint.js +8 -2
- package/dist/routes/endpoints/callEndpoint.js +32 -13
- package/dist/routes/endpoints/control/controlReject.js +3 -1
- package/dist/routes/endpoints/control/controlSetState.js +13 -2
- package/dist/routes/endpoints/control/controlThrow.js +4 -1
- package/dist/routes/endpoints/findSchedule.js +35 -0
- package/dist/routes/endpoints/forwardScheduledEndpoint.js +106 -0
- package/dist/routes/endpoints/getEndpointConfig.js +9 -3
- package/dist/routes/endpoints/getEnvironmentSchedules.js +29 -0
- package/dist/routes/endpoints/handleAgentCall.js +83 -0
- package/dist/routes/endpoints/handleEndpointCall.js +49 -1
- package/dist/routes/endpoints/handleRenderNotification.js +189 -0
- package/dist/routes/endpoints/handleValidateSchema.js +3 -1
- package/dist/routes/endpoints/isUnauthenticatedHuman.js +31 -0
- package/dist/routes/endpoints/resolveCronEnvironment.js +37 -0
- package/dist/routes/endpoints/runDetachedEndpoint.js +66 -0
- package/dist/routes/endpoints/runRoutine.js +15 -1
- package/dist/routes/endpoints/runScheduledEndpoint.js +104 -0
- package/dist/routes/endpoints/runWebhookEndpoint.js +83 -0
- package/dist/routes/endpoints/scheduleBackground.js +48 -0
- package/dist/routes/mcp/createMcpServer.js +160 -0
- package/dist/routes/notifications/derivePreview.js +30 -0
- package/dist/routes/notifications/getNotificationConfig.js +32 -0
- package/dist/routes/notifications/resolveNotificationLinks.js +72 -0
- package/dist/routes/notifications/resolveThemeLogo.js +35 -0
- package/dist/routes/page/dynamic/resolveDynamicContent.js +172 -0
- package/dist/routes/page/dynamic/unescapeOperators.js +36 -0
- package/dist/routes/page/dynamic/validateFragment.js +104 -0
- package/dist/routes/page/getPageConfig.js +16 -6
- package/dist/routes/request/callRequest.js +2 -1
- package/dist/routes/websocket/authorizeWebsocket.js +27 -0
- package/dist/routes/websocket/createChannelRegistry.js +269 -0
- package/dist/routes/websocket/createWebSocketConnection.js +131 -0
- package/dist/routes/websocket/getWebsocketConfig.js +30 -0
- package/dist/routes/websocket/getWebsocketResolver.js +33 -0
- package/dist/routes/websocket/prepareChannel.js +72 -0
- package/dist/test/testContext.js +10 -3
- package/package.json +12 -10
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import buildEndpointResult from '../../response/buildEndpointResult.js';
|
|
17
|
+
import getEndpointConfig from '../endpoints/getEndpointConfig.js';
|
|
18
|
+
import invokeEndpoint from '../endpoints/invokeEndpoint.js';
|
|
19
|
+
import authorizeAgent from './authorizeAgent.js';
|
|
20
|
+
import getAgentConfig from './getAgentConfig.js';
|
|
21
|
+
import getAgentResolver from './getAgentResolver.js';
|
|
22
|
+
import getConnectionConfig from '../connections/getConnectionConfig.js';
|
|
23
|
+
import getConnection from '../connections/getConnection.js';
|
|
24
|
+
// Shared agent invocation construction for the streaming chat route (callAgent)
|
|
25
|
+
// and the headless CallAgent routine step (handleAgentCall): loads the agent
|
|
26
|
+
// config, evaluates operators, creates the provider connection, and builds the
|
|
27
|
+
// resolver context. mode ('chat' | 'generate') selects the resolver's execution
|
|
28
|
+
// path; endpointDepth threads the endpoint call depth cap through agent tool
|
|
29
|
+
// and hook endpoint calls.
|
|
30
|
+
async function prepareAgent(context, { agentId, agentContext, endpointDepth = 0, mode = 'chat' }) {
|
|
31
|
+
const agentConfig = await getAgentConfig(context, {
|
|
32
|
+
agentId
|
|
33
|
+
});
|
|
34
|
+
authorizeAgent(context, {
|
|
35
|
+
agentConfig
|
|
36
|
+
});
|
|
37
|
+
// Evaluate operators in agent properties (e.g. _user, _secret, _payload)
|
|
38
|
+
agentConfig.properties = context.evaluateOperators({
|
|
39
|
+
input: agentConfig.properties ?? {},
|
|
40
|
+
location: agentConfig.agentId,
|
|
41
|
+
payload: agentContext,
|
|
42
|
+
state: {},
|
|
43
|
+
steps: {}
|
|
44
|
+
});
|
|
45
|
+
// Load connection config from build artifacts using agent's connectionId
|
|
46
|
+
const connectionConfig = await getConnectionConfig(context, {
|
|
47
|
+
connectionId: agentConfig.connectionId,
|
|
48
|
+
configKey: agentConfig['~k']
|
|
49
|
+
});
|
|
50
|
+
// Get connection plugin from registry
|
|
51
|
+
const connection = getConnection(context, {
|
|
52
|
+
connectionConfig
|
|
53
|
+
});
|
|
54
|
+
// Evaluate operators in connection properties
|
|
55
|
+
const connectionProperties = context.evaluateOperators({
|
|
56
|
+
input: connectionConfig.properties || {},
|
|
57
|
+
location: connectionConfig.connectionId,
|
|
58
|
+
payload: {},
|
|
59
|
+
state: {},
|
|
60
|
+
steps: {}
|
|
61
|
+
});
|
|
62
|
+
// Create connection instance (e.g., Anthropic provider)
|
|
63
|
+
const connectionInstance = connection.create({
|
|
64
|
+
connection: connectionProperties
|
|
65
|
+
});
|
|
66
|
+
// Get agent type from plugin registry
|
|
67
|
+
const agentType = getAgentResolver(context, {
|
|
68
|
+
agentConfig
|
|
69
|
+
});
|
|
70
|
+
// Build resolver context with callEndpoint that allows InternalApi endpoints
|
|
71
|
+
const resolverContext = {
|
|
72
|
+
agentContext,
|
|
73
|
+
i18n: context.i18n,
|
|
74
|
+
mode,
|
|
75
|
+
evaluateOperators: (input)=>context.evaluateOperators({
|
|
76
|
+
input,
|
|
77
|
+
location: agentConfig.agentId,
|
|
78
|
+
payload: agentContext,
|
|
79
|
+
state: {},
|
|
80
|
+
steps: {}
|
|
81
|
+
}),
|
|
82
|
+
callEndpoint: async (endpointId, { payload })=>{
|
|
83
|
+
const { error, response, status } = await invokeEndpoint(context, {
|
|
84
|
+
endpointId,
|
|
85
|
+
payload,
|
|
86
|
+
endpointDepth
|
|
87
|
+
});
|
|
88
|
+
return buildEndpointResult(context, {
|
|
89
|
+
error,
|
|
90
|
+
response,
|
|
91
|
+
status
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
getEndpointConfig: async ({ endpointId })=>{
|
|
95
|
+
return getEndpointConfig(context, {
|
|
96
|
+
endpointId
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
getAgentConfig: async ({ agentId: subAgentId })=>{
|
|
100
|
+
const subAgentConfig = await getAgentConfig(context, {
|
|
101
|
+
agentId: subAgentId
|
|
102
|
+
});
|
|
103
|
+
authorizeAgent(context, {
|
|
104
|
+
agentConfig: subAgentConfig
|
|
105
|
+
});
|
|
106
|
+
return subAgentConfig;
|
|
107
|
+
},
|
|
108
|
+
getConnectionForAgent: async ({ agentConfig: subAgentConfig })=>{
|
|
109
|
+
const subConnectionConfig = await getConnectionConfig(context, {
|
|
110
|
+
connectionId: subAgentConfig.connectionId,
|
|
111
|
+
configKey: subAgentConfig['~k']
|
|
112
|
+
});
|
|
113
|
+
const subConnection = getConnection(context, {
|
|
114
|
+
connectionConfig: subConnectionConfig
|
|
115
|
+
});
|
|
116
|
+
const subConnectionProperties = context.evaluateOperators({
|
|
117
|
+
input: subConnectionConfig.properties || {},
|
|
118
|
+
location: subConnectionConfig.connectionId,
|
|
119
|
+
payload: {},
|
|
120
|
+
state: {},
|
|
121
|
+
steps: {}
|
|
122
|
+
});
|
|
123
|
+
return subConnection.create({
|
|
124
|
+
connection: subConnectionProperties
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
resolveMcpSources: async ({ agentConfig: subAgentConfig })=>{
|
|
128
|
+
const resolvedMcp = [];
|
|
129
|
+
for (const mcpSource of subAgentConfig.mcp ?? []){
|
|
130
|
+
if (!type.isNone(mcpSource.connectionId)) {
|
|
131
|
+
const mcpConnConfig = await getConnectionConfig(context, {
|
|
132
|
+
connectionId: mcpSource.connectionId,
|
|
133
|
+
configKey: subAgentConfig['~k']
|
|
134
|
+
});
|
|
135
|
+
const mcpConnection = getConnection(context, {
|
|
136
|
+
connectionConfig: mcpConnConfig
|
|
137
|
+
});
|
|
138
|
+
const mcpConnProps = context.evaluateOperators({
|
|
139
|
+
input: mcpConnConfig.properties || {},
|
|
140
|
+
location: mcpConnConfig.connectionId,
|
|
141
|
+
payload: {},
|
|
142
|
+
state: {},
|
|
143
|
+
steps: {}
|
|
144
|
+
});
|
|
145
|
+
const mcpConfig = mcpConnection.create({
|
|
146
|
+
connection: mcpConnProps
|
|
147
|
+
});
|
|
148
|
+
const { connectionId: _, ...overrides } = mcpSource;
|
|
149
|
+
resolvedMcp.push({
|
|
150
|
+
...mcpConfig,
|
|
151
|
+
...overrides
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
resolvedMcp.push(mcpSource);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return resolvedMcp;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// Resolve MCP connection references to inline config.
|
|
161
|
+
// Agent-level overrides (like confirm) may still contain operators —
|
|
162
|
+
// handleAgentChat evaluates those via its existing evaluateOperators call.
|
|
163
|
+
agentConfig.mcp = await resolverContext.resolveMcpSources({
|
|
164
|
+
agentConfig
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
agentConfig,
|
|
168
|
+
connectionInstance,
|
|
169
|
+
agentType,
|
|
170
|
+
resolverContext
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export default prepareAgent;
|
|
@@ -20,11 +20,8 @@
|
|
|
20
20
|
};
|
|
21
21
|
function createLogger({ logger }) {
|
|
22
22
|
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (error) {
|
|
26
|
-
error.code = code;
|
|
27
|
-
}
|
|
23
|
+
// Auth.js v5 logger contract: error(error), warn(code), debug(message, metadata).
|
|
24
|
+
error: (error)=>{
|
|
28
25
|
logger.error(error);
|
|
29
26
|
},
|
|
30
27
|
warn: (code)=>{
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
*/ function slugifyPrefix(value) {
|
|
16
|
+
return String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
17
|
+
}
|
|
18
|
+
// Injects an app-specific prefix into each Auth.js cookie name so multiple apps
|
|
19
|
+
// on the same host do not share a cookie jar (browsers do not scope cookies by
|
|
20
|
+
// port). Only names are overridden — Auth.js v5 deep-merges `config.cookies`
|
|
21
|
+
// into its defaults, so cookie options (httpOnly, secure, maxAge) stay in sync
|
|
22
|
+
// with @auth/core's per-request defaults. Names mirror defaultCookies in
|
|
23
|
+
// @auth/core/lib/utils/cookie.js.
|
|
24
|
+
function createPrefixedCookies({ prefix, useSecureCookies }) {
|
|
25
|
+
const securePrefix = useSecureCookies ? '__Secure-' : '';
|
|
26
|
+
const hostPrefix = useSecureCookies ? '__Host-' : '';
|
|
27
|
+
return {
|
|
28
|
+
sessionToken: {
|
|
29
|
+
name: `${securePrefix}${prefix}.authjs.session-token`
|
|
30
|
+
},
|
|
31
|
+
callbackUrl: {
|
|
32
|
+
name: `${securePrefix}${prefix}.authjs.callback-url`
|
|
33
|
+
},
|
|
34
|
+
csrfToken: {
|
|
35
|
+
name: `${hostPrefix}${prefix}.authjs.csrf-token`
|
|
36
|
+
},
|
|
37
|
+
pkceCodeVerifier: {
|
|
38
|
+
name: `${securePrefix}${prefix}.authjs.pkce.code_verifier`
|
|
39
|
+
},
|
|
40
|
+
state: {
|
|
41
|
+
name: `${securePrefix}${prefix}.authjs.state`
|
|
42
|
+
},
|
|
43
|
+
nonce: {
|
|
44
|
+
name: `${securePrefix}${prefix}.authjs.nonce`
|
|
45
|
+
},
|
|
46
|
+
webauthnChallenge: {
|
|
47
|
+
name: `${securePrefix}${prefix}.authjs.challenge`
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export { slugifyPrefix };
|
|
52
|
+
export default createPrefixedCookies;
|
|
@@ -19,10 +19,11 @@ import createCallbacks from './callbacks/createCallbacks.js';
|
|
|
19
19
|
import createEvents from './events/createEvents.js';
|
|
20
20
|
import createLogger from './createLogger.js';
|
|
21
21
|
import createProviders from './createProviders.js';
|
|
22
|
-
|
|
22
|
+
import resolveCookies from './resolveCookies.js';
|
|
23
|
+
const authConfigCache = {};
|
|
23
24
|
let initialized = false;
|
|
24
|
-
function
|
|
25
|
-
if (initialized) return
|
|
25
|
+
function getAuthConfig({ appMeta, authJson, dev, logger, plugins, secrets }) {
|
|
26
|
+
if (initialized) return authConfigCache;
|
|
26
27
|
const operatorsParser = new ServerParser({
|
|
27
28
|
lowdefyApp: appMeta,
|
|
28
29
|
operators: {
|
|
@@ -40,36 +41,46 @@ function getNextAuthConfig({ appMeta, authJson, logger, plugins, secrets }) {
|
|
|
40
41
|
if (operatorErrors.length > 0) {
|
|
41
42
|
throw operatorErrors[0];
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
authConfigCache.adapter = createAdapter({
|
|
44
45
|
authConfig,
|
|
45
46
|
logger,
|
|
46
47
|
plugins
|
|
47
48
|
});
|
|
48
|
-
|
|
49
|
+
authConfigCache.callbacks = createCallbacks({
|
|
49
50
|
authConfig,
|
|
50
51
|
logger,
|
|
51
52
|
plugins
|
|
52
53
|
});
|
|
53
|
-
|
|
54
|
+
authConfigCache.events = createEvents({
|
|
54
55
|
authConfig,
|
|
55
56
|
logger,
|
|
56
57
|
plugins
|
|
57
58
|
});
|
|
58
|
-
|
|
59
|
+
authConfigCache.logger = createLogger({
|
|
59
60
|
logger
|
|
60
61
|
});
|
|
61
|
-
|
|
62
|
+
authConfigCache.providers = createProviders({
|
|
62
63
|
authConfig,
|
|
63
64
|
logger,
|
|
64
65
|
plugins
|
|
65
66
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
authConfigCache.debug = authConfig.debug ?? logger?.isLevelEnabled('debug') === true;
|
|
68
|
+
authConfigCache.pages = authConfig.authPages;
|
|
69
|
+
authConfigCache.session = authConfig.session;
|
|
70
|
+
authConfigCache.theme = authConfig.theme;
|
|
71
|
+
authConfigCache.cookies = resolveCookies({
|
|
72
|
+
appMeta,
|
|
73
|
+
authConfig,
|
|
74
|
+
dev
|
|
75
|
+
});
|
|
76
|
+
// Auth.js v5 reads AUTH_SECRET; the v4 NEXTAUTH_SECRET variable is not
|
|
77
|
+
// supported — deployments must rename it (see the v5-to-v6 migration doc).
|
|
78
|
+
authConfigCache.secret = process.env.AUTH_SECRET;
|
|
79
|
+
// Self-hosted servers run behind arbitrary proxies; derive URLs from request
|
|
80
|
+
// headers (v4 derived them from NEXTAUTH_URL, aliased to AUTH_URL at startup).
|
|
81
|
+
authConfigCache.trustHost = true;
|
|
82
|
+
authConfigCache.basePath = '/api/auth';
|
|
72
83
|
initialized = true;
|
|
73
|
-
return
|
|
84
|
+
return authConfigCache;
|
|
74
85
|
}
|
|
75
|
-
export default
|
|
86
|
+
export default getAuthConfig;
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import createPrefixedCookies, { slugifyPrefix } from './createPrefixedCookies.js';
|
|
17
|
+
// Cookie name resolution, in order of precedence:
|
|
18
|
+
// 1. An explicit `auth.advanced.cookies` object is used verbatim.
|
|
19
|
+
// 2. An explicit `auth.advanced.cookiePrefix` namespaces cookie names (dev and prod).
|
|
20
|
+
// 3. When running the dev server, derive a prefix from the app slug/name so multiple
|
|
21
|
+
// apps on localhost do not share a cookie jar (browsers do not scope cookies by
|
|
22
|
+
// port). Production is left untouched, returning undefined for Auth.js defaults.
|
|
23
|
+
function resolveCookies({ appMeta, authConfig, dev }) {
|
|
24
|
+
const explicitCookies = authConfig?.advanced?.cookies;
|
|
25
|
+
if (!type.isNone(explicitCookies)) return explicitCookies;
|
|
26
|
+
const devPrefix = dev ? slugifyPrefix(appMeta?.slug ?? appMeta?.name ?? '') : '';
|
|
27
|
+
const prefix = authConfig?.advanced?.cookiePrefix ?? (devPrefix === '' ? undefined : devPrefix);
|
|
28
|
+
if (type.isNone(prefix) || prefix === '') return undefined;
|
|
29
|
+
// Auth.js v5 reads AUTH_URL with NEXTAUTH_URL as a v4 fallback.
|
|
30
|
+
const authUrl = process.env.AUTH_URL ?? process.env.NEXTAUTH_URL;
|
|
31
|
+
const useSecureCookies = authUrl?.startsWith('https://') ?? false;
|
|
32
|
+
return createPrefixedCookies({
|
|
33
|
+
prefix,
|
|
34
|
+
useSecureCookies
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export default resolveCookies;
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
*/ import { ServerParser } from '@lowdefy/operators';
|
|
16
|
+
import { _app, _secret } from '@lowdefy/operators-js/operators/server';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
19
|
+
// Builds the API auth strategy verifiers from the auth.json build artifact.
|
|
20
|
+
// Build has validated the config and written all defaults, so this resolves
|
|
21
|
+
// the _secret operators and constructs each strategy type's verifier.
|
|
22
|
+
// Verifier factories warn on short static keys at construction, so key
|
|
23
|
+
// strength surfaces at startup, never as a build failure.
|
|
24
|
+
function createAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
|
|
25
|
+
if (!type.isArray(authJson.strategies) || authJson.strategies.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const operatorsParser = new ServerParser({
|
|
29
|
+
lowdefyApp: appMeta,
|
|
30
|
+
operators: {
|
|
31
|
+
_app,
|
|
32
|
+
_secret
|
|
33
|
+
},
|
|
34
|
+
secrets,
|
|
35
|
+
user: {}
|
|
36
|
+
});
|
|
37
|
+
const { output: strategiesConfig, errors: operatorErrors } = operatorsParser.parse({
|
|
38
|
+
input: authJson.strategies,
|
|
39
|
+
location: 'auth.strategies',
|
|
40
|
+
payload: {}
|
|
41
|
+
});
|
|
42
|
+
if (operatorErrors.length > 0) {
|
|
43
|
+
// Startup fails on the first error; log the rest so they can all be
|
|
44
|
+
// fixed in one pass instead of one boot per error.
|
|
45
|
+
operatorErrors.slice(1).forEach((error)=>logger.error(error));
|
|
46
|
+
throw operatorErrors[0];
|
|
47
|
+
}
|
|
48
|
+
return strategiesConfig.map((strategy)=>{
|
|
49
|
+
const strategyPlugin = plugins.strategies[strategy.type];
|
|
50
|
+
if (type.isNone(strategyPlugin)) {
|
|
51
|
+
throw new ConfigError(`Auth strategy type "${strategy.type}" not found at strategy "${strategy.id}".`, {
|
|
52
|
+
configKey: strategy['~k']
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
let verify;
|
|
56
|
+
try {
|
|
57
|
+
verify = strategyPlugin({
|
|
58
|
+
logger,
|
|
59
|
+
properties: strategy.properties,
|
|
60
|
+
strategyId: strategy.id
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new ConfigError(error.message, {
|
|
64
|
+
cause: error,
|
|
65
|
+
configKey: strategy['~k']
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
attributes: strategy.attributes,
|
|
70
|
+
id: strategy.id,
|
|
71
|
+
roles: strategy.roles,
|
|
72
|
+
type: strategy.type,
|
|
73
|
+
verify
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export default createAuthStrategies;
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
*/ import createAuthStrategies from './createAuthStrategies.js';
|
|
16
|
+
let strategies;
|
|
17
|
+
// The strategy verifiers are constructed once per process at first use -
|
|
18
|
+
// the server middleware tries them in config order on every request that
|
|
19
|
+
// resolves no session.
|
|
20
|
+
function getAuthStrategies({ appMeta, authJson, logger, plugins, secrets }) {
|
|
21
|
+
if (strategies) return strategies;
|
|
22
|
+
strategies = createAuthStrategies({
|
|
23
|
+
appMeta,
|
|
24
|
+
authJson,
|
|
25
|
+
logger,
|
|
26
|
+
plugins,
|
|
27
|
+
secrets
|
|
28
|
+
});
|
|
29
|
+
return strategies;
|
|
30
|
+
}
|
|
31
|
+
export default getAuthStrategies;
|
|
@@ -12,12 +12,22 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import {
|
|
15
|
+
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { ReservedKeyError, set } from '@lowdefy/helpers';
|
|
16
17
|
function addStepResult(context, routineContext, { result, stepId }) {
|
|
17
18
|
const key = [
|
|
18
19
|
stepId,
|
|
19
20
|
...routineContext.arrayIndices
|
|
20
21
|
].join('.');
|
|
21
|
-
|
|
22
|
+
try {
|
|
23
|
+
set(routineContext.steps, key, result);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error instanceof ReservedKeyError) {
|
|
26
|
+
throw new ConfigError(`Reserved step id "${error.segment}" cannot be used`, {
|
|
27
|
+
cause: error
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
export default addStepResult;
|
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
-
|
|
15
|
+
*/ import { AuthenticationError, ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { type } from '@lowdefy/helpers';
|
|
17
|
+
function authorizeApiEndpoint({ authorize, logger, user }, { endpointConfig }) {
|
|
17
18
|
if (!authorize(endpointConfig)) {
|
|
18
19
|
logger.debug({
|
|
19
20
|
event: 'debug_api_authorize',
|
|
20
21
|
authorized: false,
|
|
21
22
|
auth_config: endpointConfig.auth
|
|
22
23
|
});
|
|
24
|
+
// Unauthenticated on a protected endpoint - 401 tells the caller to fix
|
|
25
|
+
// its credentials. Wrong roles stay opaque below.
|
|
26
|
+
if (type.isNone(user)) {
|
|
27
|
+
throw new AuthenticationError(`Authentication required for API endpoint "${endpointConfig.endpointId}".`);
|
|
28
|
+
}
|
|
23
29
|
throw new ConfigError(`API Endpoint "${endpointConfig.endpointId}" does not exist.`);
|
|
24
30
|
}
|
|
25
31
|
logger.debug({
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { serializer } from '@lowdefy/helpers';
|
|
16
|
-
import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { AuthenticationError, ConfigError } from '@lowdefy/errors';
|
|
17
17
|
import authorizeApiEndpoint from './authorizeApiEndpoint.js';
|
|
18
|
+
import buildEndpointResult from '../../response/buildEndpointResult.js';
|
|
18
19
|
import createEvaluateOperators from '../../context/createEvaluateOperators.js';
|
|
19
20
|
import getEndpointConfig from './getEndpointConfig.js';
|
|
21
|
+
import isUnauthenticatedHuman from './isUnauthenticatedHuman.js';
|
|
20
22
|
import runRoutine from './runRoutine.js';
|
|
23
|
+
import scheduleBackground from './scheduleBackground.js';
|
|
21
24
|
async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
22
25
|
const { logger } = context;
|
|
23
26
|
context.blockId = blockId;
|
|
@@ -34,9 +37,11 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
|
34
37
|
const endpointConfig = await getEndpointConfig(context, {
|
|
35
38
|
endpointId
|
|
36
39
|
});
|
|
37
|
-
// Block HTTP access to InternalApi endpoints
|
|
40
|
+
// Block HTTP access to InternalApi endpoints - same error as a missing
|
|
41
|
+
// endpoint, including the unauthenticated fork, so an internal endpoint is
|
|
42
|
+
// indistinguishable from one that does not exist on both paths.
|
|
38
43
|
if (endpointConfig.type === 'InternalApi') {
|
|
39
|
-
const err = new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
44
|
+
const err = await isUnauthenticatedHuman(context) ? new AuthenticationError(`Authentication required for API endpoint "${endpointId}".`) : new ConfigError(`API Endpoint "${endpointId}" does not exist.`);
|
|
40
45
|
logger.debug({
|
|
41
46
|
params: {
|
|
42
47
|
endpointId
|
|
@@ -56,18 +61,32 @@ async function callEndpoint(context, { blockId, endpointId, pageId, payload }) {
|
|
|
56
61
|
state: {},
|
|
57
62
|
endpointDepth: 0
|
|
58
63
|
};
|
|
64
|
+
// async: true — acknowledge now, run the routine in the background.
|
|
65
|
+
// Auth was already checked above; the outcome lands in logs (scheduleBackground)
|
|
66
|
+
// and in whatever the routine itself records.
|
|
67
|
+
if (endpointConfig.async === true) {
|
|
68
|
+
scheduleBackground(context, {
|
|
69
|
+
event: 'background_endpoint',
|
|
70
|
+
endpointId
|
|
71
|
+
}, ()=>runRoutine(context, routineContext, {
|
|
72
|
+
routine: endpointConfig.routine
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
error: null,
|
|
76
|
+
response: serializer.serialize({
|
|
77
|
+
accepted: true
|
|
78
|
+
}),
|
|
79
|
+
status: 'accepted',
|
|
80
|
+
success: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
59
83
|
const { error, response, status } = await runRoutine(context, routineContext, {
|
|
60
84
|
routine: endpointConfig.routine
|
|
61
85
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
error: serializer.serialize(error),
|
|
68
|
-
response: serializer.serialize(response),
|
|
69
|
-
status: success ? 'success' : status,
|
|
70
|
-
success
|
|
71
|
-
};
|
|
86
|
+
return buildEndpointResult(context, {
|
|
87
|
+
error,
|
|
88
|
+
response,
|
|
89
|
+
status
|
|
90
|
+
});
|
|
72
91
|
}
|
|
73
92
|
export default callEndpoint;
|
|
@@ -37,9 +37,11 @@ async function controlReject(context, routineContext, { control }) {
|
|
|
37
37
|
cause,
|
|
38
38
|
isReject: true
|
|
39
39
|
});
|
|
40
|
+
// Log under `err` — see controlThrow: only the `err` key runs the pino error
|
|
41
|
+
// serializer, so `error` would drop the message from the log line.
|
|
40
42
|
context.logger.warn({
|
|
41
43
|
event: 'warn_control_reject',
|
|
42
|
-
error
|
|
44
|
+
err: error
|
|
43
45
|
});
|
|
44
46
|
return {
|
|
45
47
|
status: 'reject',
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import {
|
|
15
|
+
*/ import { ConfigError } from '@lowdefy/errors';
|
|
16
|
+
import { ReservedKeyError, set } from '@lowdefy/helpers';
|
|
16
17
|
function controlSetState(context, routineContext, { control }) {
|
|
17
18
|
const { logger, evaluateOperators } = context;
|
|
18
19
|
const { items } = routineContext;
|
|
@@ -30,7 +31,17 @@ function controlSetState(context, routineContext, { control }) {
|
|
|
30
31
|
evaluated: evaluatedSetState
|
|
31
32
|
});
|
|
32
33
|
Object.entries(evaluatedSetState).forEach(([key, value])=>{
|
|
33
|
-
|
|
34
|
+
try {
|
|
35
|
+
set(routineContext.state, key, value);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof ReservedKeyError) {
|
|
38
|
+
throw new ConfigError(`Reserved key "${error.segment}" cannot be used in :set_state`, {
|
|
39
|
+
cause: error,
|
|
40
|
+
configKey: control['~k']
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
34
45
|
});
|
|
35
46
|
return {
|
|
36
47
|
status: 'continue'
|
|
@@ -36,9 +36,12 @@ async function controlThrow(context, routineContext, { control }) {
|
|
|
36
36
|
const error = new UserError(message, {
|
|
37
37
|
cause
|
|
38
38
|
});
|
|
39
|
+
// Log under `err` — the pino error serializer (createNodeLogger) is registered
|
|
40
|
+
// for the `err` key only; an Error passed as `error` is JSON-dumped without its
|
|
41
|
+
// non-enumerable `message`/`stack`, producing a log line with no message.
|
|
39
42
|
context.logger.error({
|
|
40
43
|
event: 'error_control_throw',
|
|
41
|
-
error
|
|
44
|
+
err: error
|
|
42
45
|
});
|
|
43
46
|
return {
|
|
44
47
|
status: 'error',
|