@lightdash/cli 1.45.0 → 1.46.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.
Files changed (34) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +39 -4
  5. package/dist/config.test.d.ts +2 -0
  6. package/dist/config.test.d.ts.map +1 -0
  7. package/dist/config.test.js +40 -0
  8. package/dist/handlers/apps/appCodeFiles.d.ts +1 -0
  9. package/dist/handlers/apps/appCodeFiles.d.ts.map +1 -1
  10. package/dist/handlers/apps/appCodeFiles.js +7 -4
  11. package/dist/handlers/apps/appCodeFiles.test.js +8 -0
  12. package/dist/handlers/apps/preview.d.ts +56 -0
  13. package/dist/handlers/apps/preview.d.ts.map +1 -0
  14. package/dist/handlers/apps/preview.js +262 -0
  15. package/dist/handlers/apps/preview.test.d.ts +2 -0
  16. package/dist/handlers/apps/preview.test.d.ts.map +1 -0
  17. package/dist/handlers/apps/preview.test.js +189 -0
  18. package/dist/handlers/apps/previewProxy.d.ts +20 -0
  19. package/dist/handlers/apps/previewProxy.d.ts.map +1 -0
  20. package/dist/handlers/apps/previewProxy.js +144 -0
  21. package/dist/handlers/apps/previewProxy.test.d.ts +2 -0
  22. package/dist/handlers/apps/previewProxy.test.d.ts.map +1 -0
  23. package/dist/handlers/apps/previewProxy.test.js +163 -0
  24. package/dist/handlers/apps/scaffolding.test.js +7 -0
  25. package/dist/handlers/utils.d.ts +8 -0
  26. package/dist/handlers/utils.d.ts.map +1 -1
  27. package/dist/handlers/utils.js +1 -0
  28. package/dist/index.js +16 -4
  29. package/dist/vendor/authoring/AGENTS.md.tmpl +3 -3
  30. package/dist/vendor/authoring/README.md.tmpl +7 -4
  31. package/dist/vendor/authoring/developing-data-apps-locally/SKILL.md +19 -6
  32. package/dist/vendor/authoring/gitignore +1 -0
  33. package/dist/vendor/template/vite.config.js +63 -3
  34. package/package.json +3 -3
package/dist/config.d.ts CHANGED
@@ -23,6 +23,10 @@ export type Config = {
23
23
  metadataFileGitignoreNoticeShown?: boolean;
24
24
  };
25
25
  };
26
+ /** @internal Exported for filesystem-permission regression tests. */
27
+ export declare const ensureConfigFilePermissions: (filePath: string) => Promise<void>;
28
+ /** @internal Exported for filesystem-permission regression tests. */
29
+ export declare const writeConfigToFile: (filePath: string, config: Config) => Promise<void>;
26
30
  export declare const getConfig: () => Promise<Config>;
27
31
  export declare const setProject: (projectUuid: string, projectName: string) => Promise<void>;
28
32
  export declare const unsetProject: () => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,QAAsC,CAAC;AAElE,MAAM,MAAM,MAAM,GAAG;IACjB,IAAI,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACN,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,OAAO,CAAC,EAAE;QACN,qCAAqC,CAAC,EAAE,OAAO,CAAC;QAChD,gCAAgC,CAAC,EAAE,OAAO,CAAC;KAC9C,CAAC;CACL,CAAC;AAgCF,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAmBhD,CAAC;AAEF,eAAO,MAAM,UAAU,gBAAuB,MAAM,eAAe,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,YAAY,qBAUxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBAAuB,MAAM,QAAQ,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,mBAAmB,qBAU/B,CAAC;AAEF,eAAO,MAAM,cAAc,aACb,MAAM,oBACE,MAAM,kBAW3B,CAAC;AAEF,eAAO,MAAM,UAAU,YAAmB,MAAM,CAAC,SAAS,CAAC,kBAM1D,CAAC;AAEF,eAAO,MAAM,SAAS,WAAkB,MAAM,CAAC,SAAS,CAAC,kBAMxD,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,QAAsC,CAAC;AAElE,MAAM,MAAM,MAAM,GAAG;IACjB,IAAI,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACN,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,OAAO,CAAC,EAAE;QACN,qCAAqC,CAAC,EAAE,OAAO,CAAC;QAChD,gCAAgC,CAAC,EAAE,OAAO,CAAC;KAC9C,CAAC;CACL,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,2BAA2B,aAC1B,MAAM,KACjB,OAAO,CAAC,IAAI,CAgBd,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,aAChB,MAAM,UACR,MAAM,KACf,OAAO,CAAC,IAAI,CAed,CAAC;AAiCF,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAmBhD,CAAC;AAEF,eAAO,MAAM,UAAU,gBAAuB,MAAM,eAAe,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,YAAY,qBAUxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBAAuB,MAAM,QAAQ,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,mBAAmB,qBAU/B,CAAC;AAEF,eAAO,MAAM,cAAc,aACb,MAAM,oBACE,MAAM,kBAW3B,CAAC;AAEF,eAAO,MAAM,UAAU,YAAmB,MAAM,CAAC,SAAS,CAAC,kBAM1D,CAAC;AAEF,eAAO,MAAM,SAAS,WAAkB,MAAM,CAAC,SAAS,CAAC,kBAMxD,CAAC"}
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setAnswer = exports.setContext = exports.setDefaultUser = exports.unsetPreviewProject = exports.setPreviewProject = exports.unsetProject = exports.setProject = exports.getConfig = exports.configFilePath = void 0;
3
+ exports.setAnswer = exports.setContext = exports.setDefaultUser = exports.unsetPreviewProject = exports.setPreviewProject = exports.unsetProject = exports.setProject = exports.getConfig = exports.writeConfigToFile = exports.ensureConfigFilePermissions = exports.configFilePath = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_1 = require("fs");
6
6
  const yaml = tslib_1.__importStar(require("js-yaml"));
@@ -10,13 +10,48 @@ const path = tslib_1.__importStar(require("path"));
10
10
  const uuid_1 = require("uuid");
11
11
  const configDir = path.join(os.homedir(), '.config', 'lightdash');
12
12
  exports.configFilePath = path.join(configDir, 'config.yaml');
13
+ /** @internal Exported for filesystem-permission regression tests. */
14
+ const ensureConfigFilePermissions = async (filePath) => {
15
+ if (process.platform === 'win32')
16
+ return;
17
+ await fs_1.promises.chmod(path.dirname(filePath), 0o700);
18
+ try {
19
+ await fs_1.promises.chmod(filePath, 0o600);
20
+ }
21
+ catch (error) {
22
+ if (!(error instanceof Error &&
23
+ 'code' in error &&
24
+ error.code === 'ENOENT')) {
25
+ throw error;
26
+ }
27
+ }
28
+ };
29
+ exports.ensureConfigFilePermissions = ensureConfigFilePermissions;
30
+ /** @internal Exported for filesystem-permission regression tests. */
31
+ const writeConfigToFile = async (filePath, config) => {
32
+ const directory = path.dirname(filePath);
33
+ await fs_1.promises.mkdir(directory, { recursive: true, mode: 0o700 });
34
+ // Tighten an existing config before overwriting it. `mode` on writeFile
35
+ // only applies when it creates a new file.
36
+ await (0, exports.ensureConfigFilePermissions)(filePath);
37
+ await fs_1.promises.writeFile(filePath, yaml.dump(config), {
38
+ encoding: 'utf8',
39
+ mode: 0o600,
40
+ });
41
+ // `mode` only applies when mkdir/writeFile creates a path. Tighten
42
+ // existing CLI config paths as well; Windows does not implement POSIX
43
+ // permission bits, so leave ACL management to the OS there.
44
+ await (0, exports.ensureConfigFilePermissions)(filePath);
45
+ };
46
+ exports.writeConfigToFile = writeConfigToFile;
13
47
  const setConfig = async (config) => {
14
- await fs_1.promises.mkdir(path.dirname(exports.configFilePath), { recursive: true });
15
- await fs_1.promises.writeFile(exports.configFilePath, yaml.dump(config), 'utf8');
48
+ await (0, exports.writeConfigToFile)(exports.configFilePath, config);
16
49
  };
17
50
  const getRawConfig = async () => {
18
51
  try {
19
- const raw = yaml.load(await fs_1.promises.readFile(exports.configFilePath, 'utf8'));
52
+ await (0, exports.ensureConfigFilePermissions)(exports.configFilePath);
53
+ const contents = await fs_1.promises.readFile(exports.configFilePath, 'utf8');
54
+ const raw = yaml.load(contents);
20
55
  return raw;
21
56
  }
22
57
  catch (e) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../src/config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs_1 = require("fs");
5
+ const os = tslib_1.__importStar(require("os"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const config_1 = require("./config");
8
+ // Masking file-type bits is the standard way to assert POSIX permissions.
9
+ // eslint-disable-next-line no-bitwise
10
+ const permissionBits = (mode) => mode & 0o777;
11
+ const describePosix = process.platform === 'win32' ? describe.skip : describe;
12
+ describePosix('CLI config permissions', () => {
13
+ let tempDir;
14
+ let configDir;
15
+ let configPath;
16
+ beforeEach(async () => {
17
+ tempDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-config-'));
18
+ configDir = path.join(tempDir, 'lightdash');
19
+ configPath = path.join(configDir, 'config.yaml');
20
+ });
21
+ afterEach(async () => {
22
+ await fs_1.promises.rm(tempDir, { recursive: true, force: true });
23
+ });
24
+ it('creates the config directory and file with private permissions', async () => {
25
+ await (0, config_1.writeConfigToFile)(configPath, {
26
+ context: { apiKey: 'ldpat_secret' },
27
+ });
28
+ expect(permissionBits((await fs_1.promises.stat(configDir)).mode)).toBe(0o700);
29
+ expect(permissionBits((await fs_1.promises.stat(configPath)).mode)).toBe(0o600);
30
+ });
31
+ it('tightens permissions on an existing config path', async () => {
32
+ await fs_1.promises.mkdir(configDir, { recursive: true, mode: 0o755 });
33
+ await fs_1.promises.writeFile(configPath, 'context: {}', { mode: 0o644 });
34
+ await fs_1.promises.chmod(configDir, 0o755);
35
+ await fs_1.promises.chmod(configPath, 0o644);
36
+ await (0, config_1.ensureConfigFilePermissions)(configPath);
37
+ expect(permissionBits((await fs_1.promises.stat(configDir)).mode)).toBe(0o700);
38
+ expect(permissionBits((await fs_1.promises.stat(configPath)).mode)).toBe(0o600);
39
+ });
40
+ });
@@ -53,5 +53,6 @@ export declare const applySdkMirrorToTemplateDeps: (templateDependencies: Record
53
53
  * deps (payload stays identical to today's format).
54
54
  */
55
55
  export declare const attachDependenciesToCode: (code: DataAppCode, customDeps: Record<string, string>, deps: DataAppDependencies) => DataAppCode;
56
+ export declare const readManifestFromDir: (dir: string) => Promise<DataAppManifest>;
56
57
  export declare const readBundleFromDir: (dir: string) => Promise<DataAppCode>;
57
58
  //# sourceMappingURL=appCodeFiles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAC5D,wBAoBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,aACnB,eAAe,gBACX,GAAG,CAAC,MAAM,CAAC,KAC1B,MAgBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,QACL,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC3B,MAAM,KACZ,OAAO,CAAC,oBAAoB,GAAG,IAAI,CA6BrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,wBACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,MAAM,EAOH,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,yBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,eAC/B,MAAM,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAC3B,WAAW,cACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,mBAAmB,KAC1B,WAC4E,CAAC;AAEhF,eAAO,MAAM,iBAAiB,QAAe,MAAM,KAAG,OAAO,CAAC,WAAW,CAuBxE,CAAC"}
1
+ {"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAC5D,wBAoBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,aACnB,eAAe,gBACX,GAAG,CAAC,MAAM,CAAC,KAC1B,MAgBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,QACL,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC3B,MAAM,KACZ,OAAO,CAAC,oBAAoB,GAAG,IAAI,CA6BrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,wBACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,MAAM,EAOH,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,yBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,eAC/B,MAAM,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAC3B,WAAW,cACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,mBAAmB,KAC1B,WAC4E,CAAC;AAEhF,eAAO,MAAM,mBAAmB,QACvB,MAAM,KACZ,OAAO,CAAC,eAAe,CAYzB,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAe,MAAM,KAAG,OAAO,CAAC,WAAW,CAYxE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readBundleFromDir = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.resolveAppFolderName = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
3
+ exports.readBundleFromDir = exports.readManifestFromDir = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.resolveAppFolderName = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
6
  const fs_1 = require("fs");
@@ -209,15 +209,18 @@ exports.applySdkMirrorToTemplateDeps = applySdkMirrorToTemplateDeps;
209
209
  */
210
210
  const attachDependenciesToCode = (code, customDeps, deps) => Object.keys(customDeps).length > 0 ? { ...code, dependencies: deps } : code;
211
211
  exports.attachDependenciesToCode = attachDependenciesToCode;
212
- const readBundleFromDir = async (dir) => {
212
+ const readManifestFromDir = async (dir) => {
213
213
  const manifestRaw = await fs_1.promises.readFile(path.join(dir, MANIFEST_FILENAME), 'utf-8');
214
- let manifest;
215
214
  try {
216
- manifest = YAML.parse(manifestRaw);
215
+ return YAML.parse(manifestRaw);
217
216
  }
218
217
  catch (err) {
219
218
  throw new Error(`Could not parse ${MANIFEST_FILENAME}: ${(0, common_1.getErrorMessage)(err)}`);
220
219
  }
220
+ };
221
+ exports.readManifestFromDir = readManifestFromDir;
222
+ const readBundleFromDir = async (dir) => {
223
+ const manifest = await (0, exports.readManifestFromDir)(dir);
221
224
  const srcDir = path.join(dir, 'src');
222
225
  const srcExists = await fs_1.promises
223
226
  .stat(srcDir)
@@ -107,6 +107,12 @@ const bundle = {
107
107
  },
108
108
  ],
109
109
  };
110
+ it('readManifestFromDir returns just the manifest', async () => {
111
+ const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-'));
112
+ await (0, appCodeFiles_1.writeBundleToDir)(dir, bundle);
113
+ const manifest = await (0, appCodeFiles_1.readManifestFromDir)(dir);
114
+ expect(manifest).toEqual(bundle.manifest);
115
+ });
110
116
  it('writes then reads back an identical bundle', async () => {
111
117
  const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-'));
112
118
  await (0, appCodeFiles_1.writeBundleToDir)(dir, bundle);
@@ -141,8 +147,10 @@ it('upload reads back only src/ files, ignoring scaffolding and context', async
141
147
  await fs_1.promises.writeFile(path.join(dir, 'package.json'), '{"name":"x"}');
142
148
  await fs_1.promises.writeFile(path.join(dir, '.lightdash/context/semantic-layer.yml'), 'models: []');
143
149
  await fs_1.promises.writeFile(path.join(dir, '.claude/skills/x/SKILL.md'), '# skill');
150
+ await fs_1.promises.writeFile(path.join(dir, '.env.local'), 'VITE_LIGHTDASH_API_KEY=secret');
144
151
  const read = await (0, appCodeFiles_1.readBundleFromDir)(dir);
145
152
  expect(read.files.every((f) => f.path.startsWith('src/'))).toBe(true);
153
+ expect(read.files.map((f) => f.path)).not.toContain('.env.local');
146
154
  expect(read.files.map((f) => f.path).sort()).toEqual(bundle.files.map((f) => f.path).sort());
147
155
  });
148
156
  it('throws a clear error when the manifest is not valid YAML', async () => {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Environment passed to the `npm run dev` child process. Only the small set of
3
+ * host variables needed to launch a portable child process is inherited. In
4
+ * particular, no Lightdash credential or unrelated parent-process secret is
5
+ * copied into vite. The nonce is exposed to browser code as a run-scoped,
6
+ * project- and route-limited capability; it is not a Lightdash credential.
7
+ */
8
+ export declare const buildPreviewChildEnv: (args: {
9
+ serverUrl: string;
10
+ projectUuid: string;
11
+ proxyPort: number;
12
+ proxyNonce: string;
13
+ parentEnv?: NodeJS.ProcessEnv;
14
+ }) => Record<string, string>;
15
+ /**
16
+ * Older downloads have a vite.config.js that proxies /api straight to the
17
+ * instance and expects the credential in .env.local — incompatible with the
18
+ * proxy-based flow (requests would go out uncredentialed and 401).
19
+ */
20
+ export declare const assertScaffoldingSupportsPreviewProxy: (appDir: string) => Promise<void>;
21
+ export declare const resolvePreviewTarget: (args: {
22
+ pathArg?: string;
23
+ projectFlag?: string;
24
+ cwd: string;
25
+ }) => Promise<{
26
+ appDir: string;
27
+ projectUuid: string;
28
+ }>;
29
+ export declare const assertNodeModulesPresent: (appDir: string) => Promise<void>;
30
+ /**
31
+ * The login context can point at a different instance than the one the app
32
+ * was downloaded from — the credential pre-flight passes there, but the
33
+ * app's project doesn't exist, which would surface as a confusing 404
34
+ * inside the running app.
35
+ */
36
+ export declare const projectNotFoundMessage: (args: {
37
+ projectUuid: string;
38
+ serverUrl: string;
39
+ }) => string;
40
+ export declare const preflightPreviewRequest: (args: {
41
+ apiPath: string;
42
+ serverUrl: string;
43
+ authorization: string;
44
+ proxyAuthorization?: string;
45
+ fetchFn?: typeof fetch;
46
+ }) => Promise<boolean>;
47
+ export declare const removeLegacyPreviewCredential: (appDir: string) => Promise<boolean>;
48
+ type AppsPreviewOptions = {
49
+ project?: string;
50
+ url?: string;
51
+ token?: string;
52
+ verbose: boolean;
53
+ };
54
+ export declare const appsPreviewHandler: (pathArg: string | undefined, options: AppsPreviewOptions) => Promise<void>;
55
+ export {};
56
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/preview.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,SAAU;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACjC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CA2CxB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,WACtC,MAAM,KACf,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,oBAAoB,SAAgB;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACf,KAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAqBlD,CAAC;AAEF,eAAO,MAAM,wBAAwB,WACzB,MAAM,KACf,OAAO,CAAC,IAAI,CAUd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,SAAU;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACrB,KAAG,MACgT,CAAC;AAErT,eAAO,MAAM,uBAAuB,SAAgB;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,KAAG,OAAO,CAAC,OAAO,CAmBlB,CAAC;AAKF,eAAO,MAAM,6BAA6B,WAC9B,MAAM,KACf,OAAO,CAAC,OAAO,CAmBjB,CAAC;AAkBF,KAAK,kBAAkB,GAAG;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,kBAAkB,YAClB,MAAM,GAAG,SAAS,WAClB,kBAAkB,KAC5B,OAAO,CAAC,IAAI,CA+Gd,CAAC"}
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appsPreviewHandler = exports.removeLegacyPreviewCredential = exports.preflightPreviewRequest = exports.projectNotFoundMessage = exports.assertNodeModulesPresent = exports.resolvePreviewTarget = exports.assertScaffoldingSupportsPreviewProxy = exports.buildPreviewChildEnv = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@lightdash/common");
6
+ const crypto_1 = require("crypto");
7
+ const execa_1 = tslib_1.__importDefault(require("execa"));
8
+ const fs_1 = require("fs");
9
+ const path = tslib_1.__importStar(require("path"));
10
+ const config_1 = require("../../config");
11
+ const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
12
+ const styles = tslib_1.__importStar(require("../../styles"));
13
+ const apiClient_1 = require("../dbt/apiClient");
14
+ const utils_1 = require("../utils");
15
+ const appCodeFiles_1 = require("./appCodeFiles");
16
+ const previewProxy_1 = require("./previewProxy");
17
+ /**
18
+ * Environment passed to the `npm run dev` child process. Only the small set of
19
+ * host variables needed to launch a portable child process is inherited. In
20
+ * particular, no Lightdash credential or unrelated parent-process secret is
21
+ * copied into vite. The nonce is exposed to browser code as a run-scoped,
22
+ * project- and route-limited capability; it is not a Lightdash credential.
23
+ */
24
+ const buildPreviewChildEnv = (args) => {
25
+ const parentEnv = args.parentEnv ?? process.env;
26
+ const passthroughKeys = [
27
+ 'APPDATA',
28
+ 'CI',
29
+ 'COLORTERM',
30
+ 'COMSPEC',
31
+ 'ComSpec',
32
+ 'FORCE_COLOR',
33
+ 'HOME',
34
+ 'LANG',
35
+ 'LC_ALL',
36
+ 'LOCALAPPDATA',
37
+ 'NODE_OPTIONS',
38
+ 'NODE_PATH',
39
+ 'NO_COLOR',
40
+ 'PATH',
41
+ 'PATHEXT',
42
+ 'PNPM_HOME',
43
+ 'SHELL',
44
+ 'SYSTEMROOT',
45
+ 'SystemRoot',
46
+ 'TEMP',
47
+ 'TERM',
48
+ 'TMP',
49
+ 'TMPDIR',
50
+ 'USERPROFILE',
51
+ 'WINDIR',
52
+ ];
53
+ const childEnv = {};
54
+ for (const key of passthroughKeys) {
55
+ const value = parentEnv[key];
56
+ if (value !== undefined)
57
+ childEnv[key] = value;
58
+ }
59
+ return {
60
+ ...childEnv,
61
+ VITE_LIGHTDASH_URL: args.serverUrl.replace(/\/+$/, ''),
62
+ VITE_LIGHTDASH_API_KEY: args.proxyNonce,
63
+ VITE_LIGHTDASH_PROJECT_UUID: args.projectUuid,
64
+ LIGHTDASH_PREVIEW_PROXY_TARGET: `http://127.0.0.1:${args.proxyPort}`,
65
+ LIGHTDASH_PREVIEW_PROXY_NONCE: args.proxyNonce,
66
+ };
67
+ };
68
+ exports.buildPreviewChildEnv = buildPreviewChildEnv;
69
+ /**
70
+ * Older downloads have a vite.config.js that proxies /api straight to the
71
+ * instance and expects the credential in .env.local — incompatible with the
72
+ * proxy-based flow (requests would go out uncredentialed and 401).
73
+ */
74
+ const assertScaffoldingSupportsPreviewProxy = async (appDir) => {
75
+ const viteConfig = await fs_1.promises
76
+ .readFile(path.join(appDir, 'vite.config.js'), 'utf-8')
77
+ .catch(() => '');
78
+ if (!viteConfig.includes('LIGHTDASH_PREVIEW_PROXY_TARGET')) {
79
+ throw new Error(`This app's scaffolding predates the secure preview proxy. Re-download the app ('lightdash download --apps <appUuid>') to refresh vite.config.js, then run preview again.`);
80
+ }
81
+ };
82
+ exports.assertScaffoldingSupportsPreviewProxy = assertScaffoldingSupportsPreviewProxy;
83
+ const resolvePreviewTarget = async (args) => {
84
+ const appDir = args.pathArg
85
+ ? path.resolve(args.cwd, args.pathArg)
86
+ : args.cwd;
87
+ let manifest;
88
+ try {
89
+ manifest = await (0, appCodeFiles_1.readManifestFromDir)(appDir);
90
+ }
91
+ catch (err) {
92
+ if (err.code === 'ENOENT') {
93
+ throw new Error(`No lightdash-app.yml found in ${appDir}. Run this from a downloaded app folder (apps/<slug>/), or pass it as an argument: lightdash apps preview <path>.`);
94
+ }
95
+ throw err;
96
+ }
97
+ return {
98
+ appDir,
99
+ projectUuid: args.projectFlag ?? manifest.projectUuid,
100
+ };
101
+ };
102
+ exports.resolvePreviewTarget = resolvePreviewTarget;
103
+ const assertNodeModulesPresent = async (appDir) => {
104
+ const isDir = await fs_1.promises
105
+ .stat(path.join(appDir, 'node_modules'))
106
+ .then((s) => s.isDirectory())
107
+ .catch(() => false);
108
+ if (!isDir) {
109
+ throw new Error(`Dependencies are not installed. Run 'npm install' in ${appDir} first (preview does not auto-install).`);
110
+ }
111
+ };
112
+ exports.assertNodeModulesPresent = assertNodeModulesPresent;
113
+ /**
114
+ * The login context can point at a different instance than the one the app
115
+ * was downloaded from — the credential pre-flight passes there, but the
116
+ * app's project doesn't exist, which would surface as a confusing 404
117
+ * inside the running app.
118
+ */
119
+ const projectNotFoundMessage = (args) => `Project ${args.projectUuid} was not found on ${args.serverUrl} (or you don't have access). This app belongs to a different project or instance than the one you're logged into — run 'lightdash login <url>' for the server the app was downloaded from, or pass --project to preview against another project.`;
120
+ exports.projectNotFoundMessage = projectNotFoundMessage;
121
+ const preflightPreviewRequest = async (args) => {
122
+ const fetchFn = args.fetchFn ?? fetch;
123
+ try {
124
+ const res = await fetchFn(new URL(args.apiPath, args.serverUrl), {
125
+ headers: {
126
+ Authorization: args.authorization,
127
+ ...(args.proxyAuthorization
128
+ ? { 'Proxy-Authorization': args.proxyAuthorization }
129
+ : {}),
130
+ },
131
+ });
132
+ return res.ok;
133
+ }
134
+ catch (error) {
135
+ const reason = error instanceof Error ? error.message : 'Unknown network error';
136
+ throw new Error(`Could not connect to ${args.serverUrl} while starting preview: ${reason}`);
137
+ }
138
+ };
139
+ exports.preflightPreviewRequest = preflightPreviewRequest;
140
+ const LEGACY_PREVIEW_CREDENTIAL = /^[ \t]*(?:export[ \t]+)?LIGHTDASH_PREVIEW_API_KEY[ \t]*=.*(?:\r?\n|$)/gm;
141
+ const removeLegacyPreviewCredential = async (appDir) => {
142
+ const legacyEnvPath = path.join(appDir, '.env.local');
143
+ let legacyEnv;
144
+ try {
145
+ legacyEnv = await fs_1.promises.readFile(legacyEnvPath, 'utf-8');
146
+ }
147
+ catch (error) {
148
+ if (error.code === 'ENOENT')
149
+ return false;
150
+ throw error;
151
+ }
152
+ const sanitizedEnv = legacyEnv.replace(LEGACY_PREVIEW_CREDENTIAL, '');
153
+ if (sanitizedEnv === legacyEnv)
154
+ return false;
155
+ if (sanitizedEnv.trim() === '') {
156
+ await fs_1.promises.unlink(legacyEnvPath);
157
+ }
158
+ else {
159
+ await fs_1.promises.writeFile(legacyEnvPath, sanitizedEnv, 'utf-8');
160
+ }
161
+ return true;
162
+ };
163
+ exports.removeLegacyPreviewCredential = removeLegacyPreviewCredential;
164
+ // Ctrl-C is the documented way to stop the dev server, so an interrupted child
165
+ // is a normal exit rather than a crash to report. The runner may either die
166
+ // from the signal or translate it into the conventional 128+signal exit code.
167
+ const isUserStoppedDevServer = (error) => {
168
+ if (!(error instanceof Error)) {
169
+ return false;
170
+ }
171
+ const { signal, exitCode } = error;
172
+ return (signal === 'SIGINT' ||
173
+ signal === 'SIGTERM' ||
174
+ exitCode === 130 ||
175
+ exitCode === 143);
176
+ };
177
+ const appsPreviewHandler = async (pathArg, options) => {
178
+ globalState_1.default.setVerbose(options.verbose);
179
+ const config = await (0, config_1.getConfig)();
180
+ const serverUrl = options.url ?? config.context?.serverUrl;
181
+ // getConfig() already folds LIGHTDASH_API_KEY into context.apiKey, so the
182
+ // env-var path is safe. The only exposure is an explicit --token on the
183
+ // command line (visible in shell history / `ps aux`) — warn rather than
184
+ // block, since --token is a supported CI path.
185
+ const apiKey = options.token ?? config.context?.apiKey;
186
+ if (!serverUrl || !apiKey) {
187
+ throw new common_1.AuthorizationError(`Not logged in or missing server URL. Run 'lightdash login <url>' first, or set LIGHTDASH_URL and LIGHTDASH_API_KEY.`);
188
+ }
189
+ const authorization = (0, utils_1.getAuthHeader)(apiKey);
190
+ const proxyAuthorization = config.context?.proxyAuthorization;
191
+ if (options.token) {
192
+ globalState_1.default.log(styles.warning(`Passing --token on the command line exposes it in your shell history and process list. Prefer 'lightdash login' or the LIGHTDASH_API_KEY environment variable.`));
193
+ }
194
+ const target = await (0, exports.resolvePreviewTarget)({
195
+ pathArg,
196
+ projectFlag: options.project,
197
+ cwd: process.cwd(),
198
+ });
199
+ await (0, exports.assertNodeModulesPresent)(target.appDir);
200
+ await (0, exports.assertScaffoldingSupportsPreviewProxy)(target.appDir);
201
+ // Pre-flight the credential before starting vite: an expired/revoked
202
+ // token would otherwise surface as opaque query failures inside the app.
203
+ // Flag-supplied credentials (--url/--token) bypass the stored config, so
204
+ // hit the API directly with exactly what the proxy will forward.
205
+ if (!options.url && !options.token) {
206
+ await (0, apiClient_1.checkLightdashVersion)();
207
+ }
208
+ const preflight = (apiPath) => (0, exports.preflightPreviewRequest)({
209
+ apiPath,
210
+ serverUrl,
211
+ authorization,
212
+ proxyAuthorization,
213
+ });
214
+ if (!(await preflight('/api/v1/user'))) {
215
+ throw new common_1.AuthorizationError(`Your Lightdash credential was rejected by ${serverUrl}. Run 'lightdash login ${serverUrl}' to refresh it, or check the --url/--token values.`);
216
+ }
217
+ if (!(await preflight(`/api/v1/projects/${target.projectUuid}`))) {
218
+ throw new Error((0, exports.projectNotFoundMessage)({
219
+ projectUuid: target.projectUuid,
220
+ serverUrl,
221
+ }));
222
+ }
223
+ // Previous versions persisted this credential in .env.local. Remove only
224
+ // that obsolete entry, preserving any unrelated user-managed settings.
225
+ if (await (0, exports.removeLegacyPreviewCredential)(target.appDir)) {
226
+ globalState_1.default.log(styles.warning(`Removed an obsolete preview credential from ${path.join(target.appDir, '.env.local')}.`));
227
+ }
228
+ const proxyNonce = (0, crypto_1.randomBytes)(16).toString('hex');
229
+ const proxy = await (0, previewProxy_1.startPreviewProxy)({
230
+ upstreamUrl: serverUrl,
231
+ authorization,
232
+ proxyAuthorization,
233
+ projectUuid: target.projectUuid,
234
+ nonce: proxyNonce,
235
+ });
236
+ globalState_1.default.log(`Preview proxy on 127.0.0.1:${proxy.port} — your credential is not passed to the app; SDK traffic is restricted to project ${target.projectUuid}.`);
237
+ globalState_1.default.log(styles.warning(`Preview renders YOUR data with YOUR permissions and user attributes — viewers of the deployed app may see different data.`));
238
+ globalState_1.default.log(`Starting dev server (Ctrl-C to stop)…`);
239
+ try {
240
+ await (0, execa_1.default)('npm', ['run', 'dev'], {
241
+ cwd: target.appDir,
242
+ stdio: 'inherit',
243
+ extendEnv: false,
244
+ env: (0, exports.buildPreviewChildEnv)({
245
+ serverUrl,
246
+ projectUuid: target.projectUuid,
247
+ proxyPort: proxy.port,
248
+ proxyNonce,
249
+ }),
250
+ });
251
+ }
252
+ catch (e) {
253
+ if (!isUserStoppedDevServer(e)) {
254
+ throw e;
255
+ }
256
+ globalState_1.default.log('Preview stopped.');
257
+ }
258
+ finally {
259
+ await proxy.close();
260
+ }
261
+ };
262
+ exports.appsPreviewHandler = appsPreviewHandler;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=preview.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/preview.test.ts"],"names":[],"mappings":""}