@lowdefy/server-dev 5.6.0 → 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/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -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} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +122 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +61 -47
- package/package.original.json +64 -50
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +235 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
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 { Hono } from 'hono';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
|
|
20
|
+
import createErrorHandler from './errorHandler.js';
|
|
21
|
+
|
|
22
|
+
// This is the only test coverage of the hono 500 redaction path: the file here is
|
|
23
|
+
// byte-identical to its `server` and `server-e2e` counterparts, and `server` has no
|
|
24
|
+
// test script at all. Keep the three in sync when changing any of them.
|
|
25
|
+
|
|
26
|
+
// A string that must never reach a client. It is put on `received` at every
|
|
27
|
+
// level of the cause chain, so a single JSON.stringify search over the body
|
|
28
|
+
// proves the redaction reached that level.
|
|
29
|
+
const SECRET = 'SECRET-RECEIVED-VALUE';
|
|
30
|
+
|
|
31
|
+
function createLogger() {
|
|
32
|
+
return { error: jest.fn(), warn: jest.fn() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The error handler is only reachable through hono's app-level onError, so the
|
|
36
|
+
// handler is driven the way production drives it: a route that throws, with the
|
|
37
|
+
// lowdefyContext set by upstream middleware when there is one.
|
|
38
|
+
function createApp({ basePath = '', context, error, logger }) {
|
|
39
|
+
const app = new Hono();
|
|
40
|
+
app.use('*', async (c, next) => {
|
|
41
|
+
if (context) c.set('lowdefyContext', context);
|
|
42
|
+
await next();
|
|
43
|
+
});
|
|
44
|
+
app.all('*', () => {
|
|
45
|
+
throw error;
|
|
46
|
+
});
|
|
47
|
+
app.onError(createErrorHandler({ basePath, logger }));
|
|
48
|
+
return app;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createAuthenticationError(message) {
|
|
52
|
+
const error = new Error(message);
|
|
53
|
+
error.name = 'AuthenticationError';
|
|
54
|
+
error.received = SECRET;
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Two Error levels, each with a stack (every Error has one) and a recognisable
|
|
59
|
+
// `received`, plus a configKey the policy keeps.
|
|
60
|
+
function createErrorWithCause() {
|
|
61
|
+
const cause = new Error('Cause message.');
|
|
62
|
+
cause.received = `${SECRET}-cause`;
|
|
63
|
+
const error = new Error('Top message.', { cause });
|
|
64
|
+
error.received = `${SECRET}-top`;
|
|
65
|
+
error.configKey = 'page:home.blocks.0';
|
|
66
|
+
return error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('errorHandler returns 401 with only name and message for an AuthenticationError on an api path', async () => {
|
|
70
|
+
const logger = createLogger();
|
|
71
|
+
const context = { handleError: jest.fn() };
|
|
72
|
+
const res = await createApp({
|
|
73
|
+
context,
|
|
74
|
+
error: createAuthenticationError('Unauthenticated.'),
|
|
75
|
+
logger,
|
|
76
|
+
}).request('/api/request/getUsers');
|
|
77
|
+
|
|
78
|
+
expect(res.status).toEqual(401);
|
|
79
|
+
expect(await res.json()).toEqual({ name: 'AuthenticationError', message: 'Unauthenticated.' });
|
|
80
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(logger.warn.mock.calls[0][0]).toMatch(/Unauthenticated request/);
|
|
82
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
83
|
+
expect(context.handleError).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('errorHandler does not send an AuthenticationError through the redactor', async () => {
|
|
87
|
+
const res = await createApp({
|
|
88
|
+
error: createAuthenticationError('Unauthenticated.'),
|
|
89
|
+
logger: createLogger(),
|
|
90
|
+
}).request('/api/request/getUsers');
|
|
91
|
+
|
|
92
|
+
const body = await res.json();
|
|
93
|
+
expect(body['~e']).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('errorHandler returns text Unauthorized at 401 for an AuthenticationError on a page path', async () => {
|
|
97
|
+
const logger = createLogger();
|
|
98
|
+
const res = await createApp({
|
|
99
|
+
error: createAuthenticationError('Unauthenticated.'),
|
|
100
|
+
logger,
|
|
101
|
+
}).request('/home');
|
|
102
|
+
|
|
103
|
+
expect(res.status).toEqual(401);
|
|
104
|
+
expect(await res.text()).toEqual('Unauthorized');
|
|
105
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('errorHandler returns 500 with the serialized error envelope on an api path', async () => {
|
|
109
|
+
const res = await createApp({
|
|
110
|
+
error: createErrorWithCause(),
|
|
111
|
+
logger: createLogger(),
|
|
112
|
+
}).request('/api/request/getUsers');
|
|
113
|
+
|
|
114
|
+
expect(res.status).toEqual(500);
|
|
115
|
+
const body = await res.json();
|
|
116
|
+
expect(body['~e'].name).toEqual('Error');
|
|
117
|
+
expect(body['~e'].message).toEqual('Top message.');
|
|
118
|
+
expect(body['~e'].cause.message).toEqual('Cause message.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('errorHandler omits stack and received from the 500 envelope at the top level', async () => {
|
|
122
|
+
const res = await createApp({
|
|
123
|
+
error: createErrorWithCause(),
|
|
124
|
+
logger: createLogger(),
|
|
125
|
+
}).request('/api/request/getUsers');
|
|
126
|
+
|
|
127
|
+
const body = await res.json();
|
|
128
|
+
expect(body['~e'].stack).toBeUndefined();
|
|
129
|
+
expect(body['~e'].received).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// The regression test for the real bug: the four inline `delete` statements this
|
|
133
|
+
// replaces only ever reached depth 0, so a `received` or `stack` on the cause
|
|
134
|
+
// still crossed the wire. The policy is now applied at every error node.
|
|
135
|
+
test('errorHandler omits stack and received from the 500 envelope on the cause at depth 1', async () => {
|
|
136
|
+
const res = await createApp({
|
|
137
|
+
error: createErrorWithCause(),
|
|
138
|
+
logger: createLogger(),
|
|
139
|
+
}).request('/api/request/getUsers');
|
|
140
|
+
|
|
141
|
+
const body = await res.json();
|
|
142
|
+
expect(body['~e'].cause.stack).toBeUndefined();
|
|
143
|
+
expect(body['~e'].cause.received).toBeUndefined();
|
|
144
|
+
expect(JSON.stringify(body)).not.toContain(SECRET);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('errorHandler keeps configKey on the 500 envelope so the fault can be located in config', async () => {
|
|
148
|
+
const res = await createApp({
|
|
149
|
+
error: createErrorWithCause(),
|
|
150
|
+
logger: createLogger(),
|
|
151
|
+
}).request('/api/request/getUsers');
|
|
152
|
+
|
|
153
|
+
const body = await res.json();
|
|
154
|
+
expect(body['~e'].configKey).toEqual('page:home.blocks.0');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('errorHandler returns text Internal Server Error at 500 on a page path with no payload', async () => {
|
|
158
|
+
const res = await createApp({
|
|
159
|
+
error: createErrorWithCause(),
|
|
160
|
+
logger: createLogger(),
|
|
161
|
+
}).request('/home');
|
|
162
|
+
|
|
163
|
+
expect(res.status).toEqual(500);
|
|
164
|
+
expect(await res.text()).toEqual('Internal Server Error');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('errorHandler awaits context.handleError before serializing the response', async () => {
|
|
168
|
+
const context = {
|
|
169
|
+
handleError: jest.fn(async (error) => {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
171
|
+
error.awaited = true;
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
const error = createErrorWithCause();
|
|
175
|
+
const res = await createApp({ context, error, logger: createLogger() }).request(
|
|
176
|
+
'/api/request/getUsers'
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(context.handleError).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(context.handleError).toHaveBeenCalledWith(error);
|
|
181
|
+
const body = await res.json();
|
|
182
|
+
expect(body['~e'].awaited).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// createHandleError sets error.handled once it has logged. The client reads it
|
|
186
|
+
// to decide whether to POST the error back for logging, so it has to survive
|
|
187
|
+
// serialization or the error is logged a second time.
|
|
188
|
+
test('errorHandler returns handled true on the 500 envelope after handleError sets it', async () => {
|
|
189
|
+
const context = {
|
|
190
|
+
handleError: jest.fn(async (error) => {
|
|
191
|
+
error.handled = true;
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
const res = await createApp({
|
|
195
|
+
context,
|
|
196
|
+
error: createErrorWithCause(),
|
|
197
|
+
logger: createLogger(),
|
|
198
|
+
}).request('/api/request/getUsers');
|
|
199
|
+
|
|
200
|
+
const body = await res.json();
|
|
201
|
+
expect(body['~e'].handled).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('errorHandler logs with logger.error when the request has no lowdefyContext', async () => {
|
|
205
|
+
const logger = createLogger();
|
|
206
|
+
const error = createErrorWithCause();
|
|
207
|
+
const res = await createApp({ error, logger }).request('/api/request/getUsers');
|
|
208
|
+
|
|
209
|
+
expect(res.status).toEqual(500);
|
|
210
|
+
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(logger.error).toHaveBeenCalledWith(error);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('errorHandler treats a basePath prefixed api path as an api path', async () => {
|
|
215
|
+
const res = await createApp({
|
|
216
|
+
basePath: '/base',
|
|
217
|
+
error: createErrorWithCause(),
|
|
218
|
+
logger: createLogger(),
|
|
219
|
+
}).request('/base/api/request/getUsers');
|
|
220
|
+
|
|
221
|
+
expect(res.status).toEqual(500);
|
|
222
|
+
const body = await res.json();
|
|
223
|
+
expect(body['~e'].message).toEqual('Top message.');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('errorHandler treats a basePath prefixed page path as a page path', async () => {
|
|
227
|
+
const res = await createApp({
|
|
228
|
+
basePath: '/base',
|
|
229
|
+
error: createErrorWithCause(),
|
|
230
|
+
logger: createLogger(),
|
|
231
|
+
}).request('/base/home');
|
|
232
|
+
|
|
233
|
+
expect(res.status).toEqual(500);
|
|
234
|
+
expect(await res.text()).toEqual('Internal Server Error');
|
|
235
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { callAgent } from '@lowdefy/api';
|
|
18
|
+
import { translate, type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
import getPathSegments from '../lib/getPathSegments.js';
|
|
21
|
+
|
|
22
|
+
async function agentHandler(c) {
|
|
23
|
+
const context = c.get('lowdefyContext');
|
|
24
|
+
const t = (key, values) => translate({ key, values, i18n: context.i18n });
|
|
25
|
+
if (c.req.method !== 'POST') {
|
|
26
|
+
throw new Error(t('agent.runtime.methodNotAllowed'));
|
|
27
|
+
}
|
|
28
|
+
const segments = getPathSegments(c, '/api/agent/');
|
|
29
|
+
if (segments.length < 2) {
|
|
30
|
+
return c.json({ error: t('agent.runtime.invalidPath') }, 400);
|
|
31
|
+
}
|
|
32
|
+
const agentId = segments[segments.length - 1];
|
|
33
|
+
const pageId = segments.slice(0, -1).join('/');
|
|
34
|
+
context.logger.info({ event: 'call_agent', agentId, pageId });
|
|
35
|
+
const { conversationId } = c.req.query();
|
|
36
|
+
const { messages, urlQuery, sharedState } = await c.req.json();
|
|
37
|
+
if (!Array.isArray(messages)) {
|
|
38
|
+
return c.json({ error: t('agent.runtime.messagesMustBeArray') }, 400);
|
|
39
|
+
}
|
|
40
|
+
if (urlQuery != null && (typeof urlQuery !== 'object' || Array.isArray(urlQuery))) {
|
|
41
|
+
return c.json({ error: t('agent.runtime.urlQueryMustBeObject') }, 400);
|
|
42
|
+
}
|
|
43
|
+
if (sharedState != null && !type.isObject(sharedState)) {
|
|
44
|
+
return c.json({ error: t('agent.runtime.sharedStateMustBeObject') }, 400);
|
|
45
|
+
}
|
|
46
|
+
const { response: webResponse } = await callAgent(context, {
|
|
47
|
+
agentId,
|
|
48
|
+
pageId,
|
|
49
|
+
messages,
|
|
50
|
+
conversationId: conversationId ?? undefined,
|
|
51
|
+
sharedState: sharedState ?? undefined,
|
|
52
|
+
urlQuery: urlQuery ?? undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// callAgent returns a Web Response — stream its body straight through.
|
|
56
|
+
return c.body(webResponse.body, 200, {
|
|
57
|
+
'Content-Type': 'text/event-stream',
|
|
58
|
+
'Cache-Control': 'no-cache',
|
|
59
|
+
Connection: 'keep-alive',
|
|
60
|
+
'Content-Encoding': 'none',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default agentHandler;
|
|
@@ -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
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getPageConfig } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import getPathSegments from '../lib/getPathSegments.js';
|
|
20
|
+
|
|
21
|
+
// Page config as JSON for client-side SPA navigation. The first page load is
|
|
22
|
+
// served embedded in the HTML; subsequent navigations fetch from here.
|
|
23
|
+
async function apiPageHandler(c) {
|
|
24
|
+
const context = c.get('lowdefyContext');
|
|
25
|
+
const pageId = getPathSegments(c, '/api/page/').join('/');
|
|
26
|
+
// The client forwards its current query string on the fetch so Dynamic block
|
|
27
|
+
// resolution sees the same urlQuery as an initial HTML load.
|
|
28
|
+
const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
|
|
29
|
+
if (!pageConfig) {
|
|
30
|
+
context.logger.info({ event: 'api_page_not_found', pageId });
|
|
31
|
+
return c.json({ pageConfig: null }, 404);
|
|
32
|
+
}
|
|
33
|
+
context.logger.info({ event: 'api_page_view', pageId });
|
|
34
|
+
return c.json({ pageConfig });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default apiPageHandler;
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { authHandler } from '@hono/auth-js';
|
|
18
|
+
|
|
19
|
+
import authJson from '../../lib/build/auth.js';
|
|
20
|
+
import getDevSession from '../../lib/server/auth/getDevSession.js';
|
|
21
|
+
|
|
22
|
+
// Replaces pages/api/auth/[...nextauth].js. Hono routes HEAD requests through
|
|
23
|
+
// GET handlers, so the corporate-email pre-check branch must live inside the
|
|
24
|
+
// middleware, before delegating to the Auth.js handler. See:
|
|
25
|
+
// https://next-auth.js.org/tutorials/avoid-corporate-link-checking-email-provider
|
|
26
|
+
function authMiddleware() {
|
|
27
|
+
const handler = authJson.configured === true ? authHandler() : null;
|
|
28
|
+
return async function auth(c, next) {
|
|
29
|
+
if (authJson.configured !== true) {
|
|
30
|
+
return c.json({ message: 'Auth not configured' }, 404);
|
|
31
|
+
}
|
|
32
|
+
if (c.req.method === 'HEAD') {
|
|
33
|
+
return c.body(null, 200);
|
|
34
|
+
}
|
|
35
|
+
// Dev sessions (mock user, headless renderer cookie) exist outside
|
|
36
|
+
// Auth.js. getDevSession is the same function the server request
|
|
37
|
+
// context uses (lib/server/auth/session.js), so the session the browser
|
|
38
|
+
// client sees here is identical to the one requests authenticate with.
|
|
39
|
+
if (c.req.method === 'GET' && c.req.path.endsWith('/api/auth/session')) {
|
|
40
|
+
const devSession = await getDevSession(c);
|
|
41
|
+
if (devSession) {
|
|
42
|
+
return c.json(devSession);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return handler(c, next);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default authMiddleware;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { Hono } from 'hono';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
|
|
20
|
+
// Only the route wiring is under test here — getDevSession's own behaviour
|
|
21
|
+
// (mock/headless resolution, session-callback parity) is covered in
|
|
22
|
+
// lib/server/auth/getDevSession.test.mjs, and the real Auth.js engine is
|
|
23
|
+
// stubbed with a sentinel handler so "delegated to Auth.js" is observable.
|
|
24
|
+
const mockAuthHandler = jest.fn(() => (c) => c.json({ delegated: true }));
|
|
25
|
+
jest.unstable_mockModule('@hono/auth-js', () => ({
|
|
26
|
+
authHandler: mockAuthHandler,
|
|
27
|
+
}));
|
|
28
|
+
jest.unstable_mockModule('../../lib/build/auth.js', () => ({
|
|
29
|
+
default: { configured: true },
|
|
30
|
+
}));
|
|
31
|
+
const mockGetDevSession = jest.fn(async () => undefined);
|
|
32
|
+
jest.unstable_mockModule('../../lib/server/auth/getDevSession.js', () => ({
|
|
33
|
+
default: mockGetDevSession,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const { default: authMiddleware } = await import('./auth.js');
|
|
37
|
+
|
|
38
|
+
function createApp() {
|
|
39
|
+
const app = new Hono();
|
|
40
|
+
app.use('/api/auth/*', authMiddleware());
|
|
41
|
+
return app;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
mockGetDevSession.mockReset();
|
|
46
|
+
mockGetDevSession.mockResolvedValue(undefined);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('GET /api/auth/session returns the dev session so the client matches the server', async () => {
|
|
50
|
+
const devSession = {
|
|
51
|
+
user: {
|
|
52
|
+
id: 'dev-mock-user',
|
|
53
|
+
roles: ['admin'],
|
|
54
|
+
global_attributes: { company_ids: ['C-1'] },
|
|
55
|
+
},
|
|
56
|
+
hashed_id: 'abc',
|
|
57
|
+
expires: '2100-01-01T00:00:00.000Z',
|
|
58
|
+
};
|
|
59
|
+
mockGetDevSession.mockResolvedValue(devSession);
|
|
60
|
+
|
|
61
|
+
const res = await createApp().request('/api/auth/session');
|
|
62
|
+
expect(res.status).toEqual(200);
|
|
63
|
+
expect(await res.json()).toEqual(devSession);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('GET /api/auth/session delegates to Auth.js when no dev session exists', async () => {
|
|
67
|
+
const res = await createApp().request('/api/auth/session');
|
|
68
|
+
expect(res.status).toEqual(200);
|
|
69
|
+
expect(await res.json()).toEqual({ delegated: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('other auth routes delegate to Auth.js even when a dev session exists', async () => {
|
|
73
|
+
mockGetDevSession.mockResolvedValue({ user: { id: 'mock' } });
|
|
74
|
+
|
|
75
|
+
const res = await createApp().request('/api/auth/csrf');
|
|
76
|
+
expect(await res.json()).toEqual({ delegated: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('POST /api/auth/session delegates to Auth.js even when a dev session exists', async () => {
|
|
80
|
+
mockGetDevSession.mockResolvedValue({ user: { id: 'mock' } });
|
|
81
|
+
|
|
82
|
+
const res = await createApp().request('/api/auth/session', { method: 'POST' });
|
|
83
|
+
expect(await res.json()).toEqual({ delegated: true });
|
|
84
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { logClientError } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import clientErrorStore from '../../lib/docs/clientErrorStore.js';
|
|
20
|
+
|
|
21
|
+
async function clientErrorHandler(c) {
|
|
22
|
+
if (c.req.method !== 'POST') {
|
|
23
|
+
throw new Error('Only POST requests are supported.');
|
|
24
|
+
}
|
|
25
|
+
const context = c.get('lowdefyContext');
|
|
26
|
+
|
|
27
|
+
const origin = c.req.header('origin');
|
|
28
|
+
if (!origin) {
|
|
29
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
if (new URL(origin).host !== c.req.header('host')) {
|
|
33
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Dev keeps `received` in the payload — it powers error tracing in dev tools.
|
|
40
|
+
const body = await c.req.json();
|
|
41
|
+
const { error, ...response } = await logClientError(context, body);
|
|
42
|
+
|
|
43
|
+
// Feed the agent-facing feedback loop (GET /lowdefy-docs/build-status) with a
|
|
44
|
+
// serializable summary — not the full error object, which may carry
|
|
45
|
+
// non-serializable `received` values.
|
|
46
|
+
clientErrorStore.push({
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
name: error?.name ?? 'Error',
|
|
49
|
+
message: error?.message ?? null,
|
|
50
|
+
source: response.source ?? null,
|
|
51
|
+
config: response.config ?? null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return c.json(response);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default clientErrorHandler;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { runScheduledEndpoint } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import getPathSegments from '../lib/getPathSegments.js';
|
|
20
|
+
|
|
21
|
+
// Triggered by Vercel Cron in production: an HTTP GET to /api/cron/<endpointId>. Available in dev so
|
|
22
|
+
// scheduled endpoints can be triggered locally with curl. Vercel auto-sends the value of the project
|
|
23
|
+
// env var named exactly CRON_SECRET as `Authorization: Bearer <value>`, and the firing cron
|
|
24
|
+
// expression in the `x-vercel-cron-schedule` header. Fails closed if CRON_SECRET is unset. A request
|
|
25
|
+
// forwarded from the production deployment of another environment (see cronForward.js) also names
|
|
26
|
+
// the environment whose schedules apply in `x-lowdefy-cron-environment`.
|
|
27
|
+
async function cronHandler(c) {
|
|
28
|
+
if (c.req.method !== 'GET') {
|
|
29
|
+
throw new Error('Only GET requests are supported.');
|
|
30
|
+
}
|
|
31
|
+
const context = c.get('lowdefyContext');
|
|
32
|
+
|
|
33
|
+
const secret = process.env.CRON_SECRET;
|
|
34
|
+
if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
|
|
35
|
+
context.logger.warn({ event: 'cron_unauthorized' });
|
|
36
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const endpointId = getPathSegments(c, '/api/cron/').join('/');
|
|
40
|
+
const cron = c.req.header('x-vercel-cron-schedule');
|
|
41
|
+
const environment = c.req.header('x-lowdefy-cron-environment');
|
|
42
|
+
context.logger.info({ event: 'call_cron_endpoint', endpointId, cron, environment });
|
|
43
|
+
const response = await runScheduledEndpoint(context, { endpointId, cron, environment });
|
|
44
|
+
return c.json(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default cronHandler;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { forwardScheduledEndpoint } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import getPathSegments from '../lib/getPathSegments.js';
|
|
20
|
+
|
|
21
|
+
// Triggered by Vercel Cron on the production deployment: an HTTP GET to
|
|
22
|
+
// /api/cron-forward/<environment>/<endpointId>, registered by `lowdefy vercel-output` for the
|
|
23
|
+
// schedules of every environment declared with a url in config.cron.environments. Vercel fires crons
|
|
24
|
+
// only on production, so production pings that environment's own /api/cron/<endpointId> (with the
|
|
25
|
+
// environment's CRON_SECRET) and answers immediately. Same transport auth as cron (CRON_SECRET,
|
|
26
|
+
// fails closed).
|
|
27
|
+
async function cronForwardHandler(c) {
|
|
28
|
+
if (c.req.method !== 'GET') {
|
|
29
|
+
throw new Error('Only GET requests are supported.');
|
|
30
|
+
}
|
|
31
|
+
const context = c.get('lowdefyContext');
|
|
32
|
+
|
|
33
|
+
const secret = process.env.CRON_SECRET;
|
|
34
|
+
if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
|
|
35
|
+
context.logger.warn({ event: 'cron_forward_unauthorized' });
|
|
36
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const [environment, ...endpointSegments] = getPathSegments(c, '/api/cron-forward/');
|
|
40
|
+
const endpointId = endpointSegments.join('/');
|
|
41
|
+
const cron = c.req.header('x-vercel-cron-schedule');
|
|
42
|
+
context.logger.info({ event: 'call_cron_forward', environment, endpointId, cron });
|
|
43
|
+
const response = await forwardScheduledEndpoint(context, { environment, endpointId, cron });
|
|
44
|
+
return c.json(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default cronForwardHandler;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { runDetachedEndpoint } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import getPathSegments from '../lib/getPathSegments.js';
|
|
20
|
+
|
|
21
|
+
// Target of a CallApi step with `detached: true`: the calling invocation
|
|
22
|
+
// fire-and-forgets a POST here, so the endpoint runs in its OWN function
|
|
23
|
+
// invocation with a fresh duration budget (chainable bounded work without a
|
|
24
|
+
// queue). Same transport auth as cron (CRON_SECRET, fails closed) and the same
|
|
25
|
+
// system execution context — InternalApi endpoints are callable, `_user` is
|
|
26
|
+
// undefined. Delivery is at-most-once with no retry: targets must be
|
|
27
|
+
// idempotent.
|
|
28
|
+
async function detachedHandler(c) {
|
|
29
|
+
if (c.req.method !== 'POST') {
|
|
30
|
+
throw new Error('Only POST requests are supported.');
|
|
31
|
+
}
|
|
32
|
+
const context = c.get('lowdefyContext');
|
|
33
|
+
|
|
34
|
+
const secret = process.env.CRON_SECRET;
|
|
35
|
+
if (!secret || c.req.header('authorization') !== `Bearer ${secret}`) {
|
|
36
|
+
context.logger.warn({ event: 'detached_unauthorized' });
|
|
37
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endpointId = getPathSegments(c, '/api/detached/').join('/');
|
|
41
|
+
const { payload } = await c.req.json();
|
|
42
|
+
context.logger.info({ event: 'call_detached_endpoint', endpointId });
|
|
43
|
+
const response = await runDetachedEndpoint(context, { endpointId, payload });
|
|
44
|
+
return c.json(response);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default detachedHandler;
|