@lightdash/cli 2.599.0 → 2.600.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewProxy.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/previewProxy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"previewProxy.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/previewProxy.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,0BAA0B,8BAA8B,CAAC;AA6CtE,MAAM,MAAM,kBAAkB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAyCF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,SAAU;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACjB,KAAG,OAAO,CAAC,kBAAkB,CAsM7B,CAAC"}
|
|
@@ -49,6 +49,20 @@ const sendJsonError = (res, status, message) => {
|
|
|
49
49
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
50
50
|
res.end(JSON.stringify({ status: 'error', results: null, message }));
|
|
51
51
|
};
|
|
52
|
+
const readBody = (req) => new Promise((resolve, reject) => {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
55
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
});
|
|
58
|
+
const parseJson = (text) => {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
52
66
|
const secretsMatch = (actual, expected) => {
|
|
53
67
|
if (typeof actual !== 'string')
|
|
54
68
|
return false;
|
|
@@ -68,6 +82,50 @@ const startPreviewProxy = (args) => {
|
|
|
68
82
|
(0, credentials_1.refuseAgentConnection)();
|
|
69
83
|
const upstream = new URL(args.upstreamUrl);
|
|
70
84
|
const requestFn = upstream.protocol === 'https:' ? https.request : http.request;
|
|
85
|
+
const withUpstreamCredentials = (headers) => ({
|
|
86
|
+
...headers,
|
|
87
|
+
authorization: args.authorization,
|
|
88
|
+
...(args.proxyAuthorization
|
|
89
|
+
? { 'proxy-authorization': args.proxyAuthorization }
|
|
90
|
+
: {}),
|
|
91
|
+
});
|
|
92
|
+
// Resolves the parsed body of a successful upstream GET, or null.
|
|
93
|
+
const getUpstreamJson = (path) => new Promise((resolve) => {
|
|
94
|
+
const upstreamReq = requestFn({
|
|
95
|
+
protocol: upstream.protocol,
|
|
96
|
+
hostname: upstream.hostname,
|
|
97
|
+
port: upstream.port || undefined,
|
|
98
|
+
path,
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: withUpstreamCredentials({
|
|
101
|
+
accept: 'application/json',
|
|
102
|
+
}),
|
|
103
|
+
}, (upstreamRes) => {
|
|
104
|
+
readBody(upstreamRes).then((text) => {
|
|
105
|
+
const status = upstreamRes.statusCode ?? 502;
|
|
106
|
+
resolve(status >= 200 && status < 300
|
|
107
|
+
? parseJson(text)
|
|
108
|
+
: null);
|
|
109
|
+
}, () => resolve(null));
|
|
110
|
+
});
|
|
111
|
+
upstreamReq.on('error', () => resolve(null));
|
|
112
|
+
upstreamReq.end();
|
|
113
|
+
});
|
|
114
|
+
// Host-answered virtual route: replies with only the explore's base
|
|
115
|
+
// table, never the explore itself.
|
|
116
|
+
const answerExploreBaseTable = async (req, res) => {
|
|
117
|
+
const request = (0, common_1.parseAppSdkExploreBaseTableRequest)(parseJson(await readBody(req)));
|
|
118
|
+
if (request === null) {
|
|
119
|
+
sendJsonError(res, 400, 'Preview proxy: invalid explore base-table request');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const explore = await getUpstreamJson(`/api/v1/projects/${encodeURIComponent(args.projectUuid)}/explores/${encodeURIComponent(request.exploreName)}`);
|
|
123
|
+
const results = {
|
|
124
|
+
baseTable: (0, common_1.extractExploreBaseTable)(explore),
|
|
125
|
+
};
|
|
126
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ status: 'ok', results }));
|
|
128
|
+
};
|
|
71
129
|
const server = http.createServer((req, res) => {
|
|
72
130
|
const method = req.method ?? 'GET';
|
|
73
131
|
const nonceHeader = req.headers[exports.PREVIEW_PROXY_NONCE_HEADER];
|
|
@@ -85,6 +143,14 @@ const startPreviewProxy = (args) => {
|
|
|
85
143
|
sendJsonError(res, 400, 'Preview proxy: malformed request URL');
|
|
86
144
|
return;
|
|
87
145
|
}
|
|
146
|
+
if (pathname === common_1.APP_SDK_EXPLORE_BASE_TABLE_PATH) {
|
|
147
|
+
answerExploreBaseTable(req, res).catch(() => {
|
|
148
|
+
if (!res.headersSent) {
|
|
149
|
+
sendJsonError(res, 400, 'Preview proxy: invalid explore base-table request');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
88
154
|
// Same authority as the deployed app's postMessage bridge: anything
|
|
89
155
|
// the bridge would reject, preview rejects too.
|
|
90
156
|
if (!(0, common_1.isAllowedAppSdkRoute)(method, pathname)) {
|
|
@@ -97,11 +163,7 @@ const startPreviewProxy = (args) => {
|
|
|
97
163
|
sendJsonError(res, 403, `Preview proxy: request targets project ${routeProjectUuid}, but this preview is pinned to ${args.projectUuid}.`);
|
|
98
164
|
return;
|
|
99
165
|
}
|
|
100
|
-
const headers = filterHeaders(req.headers, DROPPED_REQUEST_HEADERS);
|
|
101
|
-
headers.authorization = args.authorization;
|
|
102
|
-
if (args.proxyAuthorization) {
|
|
103
|
-
headers['proxy-authorization'] = args.proxyAuthorization;
|
|
104
|
-
}
|
|
166
|
+
const headers = withUpstreamCredentials(filterHeaders(req.headers, DROPPED_REQUEST_HEADERS));
|
|
105
167
|
// The previewed app fetches the export's fileUrl directly from the
|
|
106
168
|
// local dev page, which carries no instance credentials — ask the
|
|
107
169
|
// backend for a SIGNED URL, same as the deployed app's bridge.
|
|
@@ -177,6 +177,32 @@ export default defineConfig(({ mode }) => {
|
|
|
177
177
|
// so all API traffic must pass through the allowlisted /api proxy.
|
|
178
178
|
// script-src/style-src stay permissive because Vite's dev runtime needs
|
|
179
179
|
// inline + eval, so this is NOT a full stand-in for the deployed sandbox.
|
|
180
|
+
// Under `lightdash apps preview`, requests go to the CLI's loopback proxy,
|
|
181
|
+
// which checks the route against the data-app SDK allowlist and attaches
|
|
182
|
+
// the credential before forwarding to Lightdash. Without the CLI (bare
|
|
183
|
+
// `npm run dev`) requests go to the instance uncredentialed and fail with
|
|
184
|
+
// 401 — preview requires the CLI command.
|
|
185
|
+
const previewProxy = {
|
|
186
|
+
target: previewProxyTarget || lightdashUrl,
|
|
187
|
+
changeOrigin: true,
|
|
188
|
+
secure: true,
|
|
189
|
+
configure: (proxy) => {
|
|
190
|
+
if (!previewProxyNonce) return;
|
|
191
|
+
proxy.on('proxyReq', (proxyReq, req) => {
|
|
192
|
+
// Do not turn Vite into an unauthenticated front door to the
|
|
193
|
+
// nonce-protected CLI proxy. The SDK presents the run-scoped
|
|
194
|
+
// nonce as its local API key; only matching requests receive
|
|
195
|
+
// the inner proxy header.
|
|
196
|
+
if (req.headers.authorization === `ApiKey ${previewProxyNonce}`) {
|
|
197
|
+
proxyReq.setHeader(
|
|
198
|
+
'x-lightdash-preview-nonce',
|
|
199
|
+
previewProxyNonce,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
180
206
|
const previewCsp = [
|
|
181
207
|
`default-src 'none'`,
|
|
182
208
|
`script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:`,
|
|
@@ -207,36 +233,10 @@ export default defineConfig(({ mode }) => {
|
|
|
207
233
|
cors: false,
|
|
208
234
|
headers: { 'Content-Security-Policy': previewCsp },
|
|
209
235
|
proxy: {
|
|
210
|
-
'/api':
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// before forwarding to Lightdash. Without the CLI (bare
|
|
215
|
-
// `npm run dev`) requests go to the instance uncredentialed
|
|
216
|
-
// and fail with 401 — preview requires the CLI command.
|
|
217
|
-
target: previewProxyTarget || lightdashUrl,
|
|
218
|
-
changeOrigin: true,
|
|
219
|
-
secure: true,
|
|
220
|
-
configure: (proxy) => {
|
|
221
|
-
if (!previewProxyNonce) return;
|
|
222
|
-
proxy.on('proxyReq', (proxyReq, req) => {
|
|
223
|
-
// Do not turn Vite into an unauthenticated front
|
|
224
|
-
// door to the nonce-protected CLI proxy. The SDK
|
|
225
|
-
// presents the run-scoped nonce as its local API
|
|
226
|
-
// key; only matching requests receive the inner
|
|
227
|
-
// proxy header.
|
|
228
|
-
if (
|
|
229
|
-
req.headers.authorization ===
|
|
230
|
-
`ApiKey ${previewProxyNonce}`
|
|
231
|
-
) {
|
|
232
|
-
proxyReq.setHeader(
|
|
233
|
-
'x-lightdash-preview-nonce',
|
|
234
|
-
previewProxyNonce,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
},
|
|
239
|
-
},
|
|
236
|
+
'/api': previewProxy,
|
|
237
|
+
// SDK virtual routes (e.g. explore base table) are answered
|
|
238
|
+
// by the CLI proxy itself, never by the instance.
|
|
239
|
+
'/__sdk': previewProxy,
|
|
240
240
|
},
|
|
241
241
|
},
|
|
242
242
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.600.0",
|
|
4
4
|
"description": "Lightdash CLI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"@actions/core": "1.11.1",
|
|
23
23
|
"@aws-sdk/credential-providers": "3.995.0",
|
|
24
24
|
"@casl/ability": "6.8.0",
|
|
25
|
-
"@lightdash/common": "2.
|
|
26
|
-
"@lightdash/warehouses": "2.
|
|
25
|
+
"@lightdash/common": "2.600.0",
|
|
26
|
+
"@lightdash/warehouses": "2.600.0",
|
|
27
27
|
"@sidvind/better-ajv-errors": "7.0.0",
|
|
28
28
|
"@types/columnify": "1.5.1",
|
|
29
29
|
"ajv": "8.18.0",
|