@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":"AAcA,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;AAyBF;;;;;;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,CAsH7B,CAAC"}
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
- // Under `lightdash apps preview`, /api goes to the CLI's
212
- // loopback proxy, which checks the route against the
213
- // data-app SDK allowlist and attaches the credential
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.599.0",
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.599.0",
26
- "@lightdash/warehouses": "2.599.0",
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",