@askdb/studio 0.2.0-beta.4 → 0.2.0-beta.41
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/README.md +33 -29
- package/bin/askdb-studio.js +3 -1
- package/dist/bin.d.ts +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +24 -3
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -15
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-Bl9jqOjj.css +2 -0
- package/dist/client/assets/index-CGC5sKu1.js +100 -0
- package/dist/client/index.html +2 -2
- package/dist/execute-registry.d.ts +89 -0
- package/dist/execute-registry.d.ts.map +1 -0
- package/dist/execute-registry.js +445 -0
- package/dist/execute-registry.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +35 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +106 -0
- package/dist/introspection.js.map +1 -0
- package/dist/package-manager.d.ts +44 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +86 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/prisma-install.d.ts +7 -0
- package/dist/prisma-install.d.ts.map +1 -0
- package/dist/prisma-install.js +7 -0
- package/dist/prisma-install.js.map +1 -0
- package/dist/request-guard.d.ts +44 -0
- package/dist/request-guard.d.ts.map +1 -0
- package/dist/request-guard.js +170 -0
- package/dist/request-guard.js.map +1 -0
- package/dist/server.d.ts +22 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1123 -148
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +77 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +511 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/api.d.ts +176 -3
- package/dist/shared/api.d.ts.map +1 -1
- package/package.json +81 -30
- package/dist/client/assets/index-Bsn2S5X2.css +0 -2
- package/dist/client/assets/index-C_RfRKkd.js +0 -8
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { getAskDbRuntimeConfig } from "@askdb/config";
|
|
3
|
+
import { connectorPackageMissingMessage, createConnectorRegistry, introspect, } from "@askdb/introspect";
|
|
4
|
+
import { createOptionalDriverLoader, rethrowDriverImportError } from "@askdb/introspect/kit";
|
|
5
|
+
import { postgresConnectorProvider } from "@askdb/postgres";
|
|
6
|
+
import { mysqlConnectorProvider } from "@askdb/mysql";
|
|
7
|
+
import { sqliteConnectorProvider } from "@askdb/sqlite";
|
|
8
|
+
import { sqlServerConnectorProvider } from "@askdb/sqlserver";
|
|
9
|
+
import { PRISMA_BUILD_APPROVALS } from "./prisma-install.js";
|
|
10
|
+
/**
|
|
11
|
+
* `@askdb/prisma` is an optional peer of Studio, as of `askdb` (#259). It is imported
|
|
12
|
+
* only when the config selects the prisma engine, from Studio's install first, then
|
|
13
|
+
* from the project in the working directory. The CLI registers it the same way.
|
|
14
|
+
*/
|
|
15
|
+
const prismaPackage = createOptionalDriverLoader({
|
|
16
|
+
packageName: "@askdb/prisma",
|
|
17
|
+
importDriver: () => import("@askdb/prisma").catch(rethrowDriverImportError),
|
|
18
|
+
missingMessage: connectorPackageMissingMessage({
|
|
19
|
+
provider: "prisma",
|
|
20
|
+
packageName: "@askdb/prisma",
|
|
21
|
+
allowBuilds: PRISMA_BUILD_APPROVALS,
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
const connectorRegistry = createConnectorRegistry([
|
|
25
|
+
postgresConnectorProvider,
|
|
26
|
+
mysqlConnectorProvider,
|
|
27
|
+
sqliteConnectorProvider,
|
|
28
|
+
sqlServerConnectorProvider,
|
|
29
|
+
{ provider: "prisma", load: async () => (await prismaPackage.load()).prismaConnectorProvider },
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Resolve what a server-side introspection run would do, from the runtime
|
|
33
|
+
* config alone: the config's provider (its adapter loaded), and the connection its adapter's
|
|
34
|
+
* `resolveConnection` hook resolves (the same registry call the CLI makes).
|
|
35
|
+
* Never includes credentials in `sourceLabel` — it is shown in the UI.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveStudioIntrospectionPlan() {
|
|
38
|
+
const rt = getAskDbRuntimeConfig();
|
|
39
|
+
const engine = rt.introspection.provider;
|
|
40
|
+
if (!connectorRegistry.hasProvider(engine)) {
|
|
41
|
+
return { ok: false, engine: null, error: `Unsupported introspection provider: ${engine}` };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// Imports an optional engine package (`@askdb/prisma`); a missing one is the plan's error.
|
|
45
|
+
await connectorRegistry.load(engine);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { ok: false, engine, error: error instanceof Error ? error.message : String(error) };
|
|
49
|
+
}
|
|
50
|
+
const connection = resolveConnection(engine);
|
|
51
|
+
if (!connection.ok)
|
|
52
|
+
return { ok: false, engine, error: connection.error };
|
|
53
|
+
return { ok: true, engine, sourceLabel: connection.sourceLabel };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The engine's adapter resolves its connection from askdb.config/env (the same
|
|
57
|
+
* hook the CLI uses) and phrases missing-config errors in terms of config keys.
|
|
58
|
+
*/
|
|
59
|
+
function resolveConnection(engine) {
|
|
60
|
+
return connectorRegistry.resolveConnection(engine, {
|
|
61
|
+
runtime: getAskDbRuntimeConfig(),
|
|
62
|
+
surface: "studio",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run introspection server-side into `outDir`, preserving any existing
|
|
67
|
+
* enrichment markdown (the writer keeps `tables/*.md`, `concepts.md`, and
|
|
68
|
+
* `tenant-policy.md` when `existingArtifactDir` is supplied — the same
|
|
69
|
+
* guarantee `askdb introspect` gives).
|
|
70
|
+
*/
|
|
71
|
+
export async function runStudioIntrospection(options) {
|
|
72
|
+
const plan = await resolveStudioIntrospectionPlan();
|
|
73
|
+
if (!plan.ok)
|
|
74
|
+
throw new Error(plan.error);
|
|
75
|
+
const connection = resolveConnection(plan.engine);
|
|
76
|
+
if (!connection.ok)
|
|
77
|
+
throw new Error(connection.error);
|
|
78
|
+
const schemaId = options.schemaId ?? inferSchemaId(options.outDir);
|
|
79
|
+
// Resync reads the same schema list `askdb introspect` does (introspection.schemas).
|
|
80
|
+
const schemas = getAskDbRuntimeConfig().introspection.schemas;
|
|
81
|
+
const connectorConfig = {
|
|
82
|
+
provider: plan.engine,
|
|
83
|
+
...connection.connection,
|
|
84
|
+
schemaId,
|
|
85
|
+
...(schemas ? { filters: { schemas } } : {}),
|
|
86
|
+
};
|
|
87
|
+
const runConfig = connectorRegistry.createConnector(connectorConfig);
|
|
88
|
+
const connector = runConfig.connector;
|
|
89
|
+
const result = await introspect(runConfig.input, {
|
|
90
|
+
outDir: options.outDir,
|
|
91
|
+
schemaId,
|
|
92
|
+
existingArtifactDir: options.hasExistingArtifact ? options.outDir : undefined,
|
|
93
|
+
}, { connector });
|
|
94
|
+
return {
|
|
95
|
+
engine: plan.engine,
|
|
96
|
+
schemaId,
|
|
97
|
+
tables: result.schema.schemas.reduce((sum, namespace) => sum + namespace.tables.length, 0),
|
|
98
|
+
warnings: result.warnings.map((warning) => typeof warning === "string" ? warning : JSON.stringify(warning)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function inferSchemaId(path) {
|
|
102
|
+
const name = basename(path);
|
|
103
|
+
const inferred = name.endsWith(".schema") ? name.slice(0, -".schema".length) : name;
|
|
104
|
+
return inferred || "introspected";
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=introspection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.js","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-manager helpers shared by the setup wizard (`setup.ts`) and the
|
|
3
|
+
* execute-driver install endpoint (`server.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Only allowlisted package names ever reach these helpers — never
|
|
6
|
+
* browser-supplied strings — but {@link packageManagerSpawnSpec} re-checks
|
|
7
|
+
* every argument anyway because Windows needs `shell: true` (see below).
|
|
8
|
+
*/
|
|
9
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
10
|
+
/** The package manager implied by a lockfile directly inside `dir`, if any. */
|
|
11
|
+
export declare function lockfilePackageManager(dir: string): PackageManager | null;
|
|
12
|
+
/**
|
|
13
|
+
* Arguments that add `packages` as dependencies with the given package manager.
|
|
14
|
+
* `allowBuilds` are dependencies whose install scripts pnpm must be told to run
|
|
15
|
+
* (`--allow-build=<name>`; pnpm 11 exits 1 on an unapproved one). npm and Yarn
|
|
16
|
+
* run install scripts, and Bun skips untrusted ones without failing, so only
|
|
17
|
+
* pnpm gets them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function packageManagerAddArgs(pm: PackageManager, packages: readonly string[], allowBuilds?: readonly string[]): string[];
|
|
20
|
+
/**
|
|
21
|
+
* The build approvals to pass `pm` in `dir`: `allowBuilds`, or none for a pnpm older
|
|
22
|
+
* than 10.4.0, whose `add` rejects `--allow-build` (pnpm 9 runs install scripts, and
|
|
23
|
+
* 10.0 to 10.3 skip unapproved ones without failing). A pnpm whose version can't be
|
|
24
|
+
* read keeps them. `askdb init` applies the same rule (`apps/cli/src/init.ts`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function supportedBuildApprovals(pm: PackageManager, dir: string, allowBuilds: readonly string[]): string[];
|
|
27
|
+
/** Human-readable install command, e.g. `pnpm add pg`. */
|
|
28
|
+
export declare function formatInstallCommand(pm: PackageManager, packages: readonly string[], allowBuilds?: readonly string[]): string;
|
|
29
|
+
export type PackageManagerSpawnSpec = {
|
|
30
|
+
command: string;
|
|
31
|
+
args: string[];
|
|
32
|
+
shell: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* How to spawn a package manager on this platform.
|
|
36
|
+
*
|
|
37
|
+
* On Windows, package managers are `.cmd` shims, and Node refuses to spawn
|
|
38
|
+
* `.cmd`/`.bat` files without a shell (EINVAL since Node 18.20.2 / 20.12.2,
|
|
39
|
+
* CVE-2024-27980). So Windows gets `shell: true` with the bare command name.
|
|
40
|
+
* Arguments are validated against a strict allowlist pattern first, so the
|
|
41
|
+
* shell never sees anything it could interpret.
|
|
42
|
+
*/
|
|
43
|
+
export declare function packageManagerSpawnSpec(pm: PackageManager, args: readonly string[], platform?: NodeJS.Platform): PackageManagerSpawnSpec;
|
|
44
|
+
//# sourceMappingURL=package-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7D,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMzE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,MAAM,EAAE,CAIV;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAMjH;AAQD,0DAA0D;AAC1D,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,MAAM,CAER;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAQF;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,cAAc,EAClB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,uBAAuB,CAUzB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-manager helpers shared by the setup wizard (`setup.ts`) and the
|
|
3
|
+
* execute-driver install endpoint (`server.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Only allowlisted package names ever reach these helpers — never
|
|
6
|
+
* browser-supplied strings — but {@link packageManagerSpawnSpec} re-checks
|
|
7
|
+
* every argument anyway because Windows needs `shell: true` (see below).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
/** The package manager implied by a lockfile directly inside `dir`, if any. */
|
|
13
|
+
export function lockfilePackageManager(dir) {
|
|
14
|
+
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
15
|
+
return "pnpm";
|
|
16
|
+
if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")))
|
|
17
|
+
return "bun";
|
|
18
|
+
if (existsSync(join(dir, "yarn.lock")))
|
|
19
|
+
return "yarn";
|
|
20
|
+
if (existsSync(join(dir, "package-lock.json")))
|
|
21
|
+
return "npm";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Arguments that add `packages` as dependencies with the given package manager.
|
|
26
|
+
* `allowBuilds` are dependencies whose install scripts pnpm must be told to run
|
|
27
|
+
* (`--allow-build=<name>`; pnpm 11 exits 1 on an unapproved one). npm and Yarn
|
|
28
|
+
* run install scripts, and Bun skips untrusted ones without failing, so only
|
|
29
|
+
* pnpm gets them.
|
|
30
|
+
*/
|
|
31
|
+
export function packageManagerAddArgs(pm, packages, allowBuilds = []) {
|
|
32
|
+
if (pm === "npm")
|
|
33
|
+
return ["install", "--save", ...packages];
|
|
34
|
+
if (pm === "pnpm")
|
|
35
|
+
return ["add", ...allowBuilds.map((name) => `--allow-build=${name}`), ...packages];
|
|
36
|
+
return ["add", ...packages];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The build approvals to pass `pm` in `dir`: `allowBuilds`, or none for a pnpm older
|
|
40
|
+
* than 10.4.0, whose `add` rejects `--allow-build` (pnpm 9 runs install scripts, and
|
|
41
|
+
* 10.0 to 10.3 skip unapproved ones without failing). A pnpm whose version can't be
|
|
42
|
+
* read keeps them. `askdb init` applies the same rule (`apps/cli/src/init.ts`).
|
|
43
|
+
*/
|
|
44
|
+
export function supportedBuildApprovals(pm, dir, allowBuilds) {
|
|
45
|
+
if (pm !== "pnpm" || allowBuilds.length === 0)
|
|
46
|
+
return [...allowBuilds];
|
|
47
|
+
const match = /^(\d+)\.(\d+)\./.exec(pnpmVersion(dir) ?? "");
|
|
48
|
+
if (!match)
|
|
49
|
+
return [...allowBuilds];
|
|
50
|
+
const [major, minor] = [Number(match[1]), Number(match[2])];
|
|
51
|
+
return major > 10 || (major === 10 && minor >= 4) ? [...allowBuilds] : [];
|
|
52
|
+
}
|
|
53
|
+
function pnpmVersion(dir) {
|
|
54
|
+
const { command, args, shell } = packageManagerSpawnSpec("pnpm", ["--version"]);
|
|
55
|
+
const result = spawnSync(command, args, { cwd: dir, encoding: "utf8", shell, stdio: ["ignore", "pipe", "ignore"], timeout: 15_000 });
|
|
56
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
57
|
+
}
|
|
58
|
+
/** Human-readable install command, e.g. `pnpm add pg`. */
|
|
59
|
+
export function formatInstallCommand(pm, packages, allowBuilds = []) {
|
|
60
|
+
return [pm, ...packageManagerAddArgs(pm, packages, allowBuilds)].join(" ");
|
|
61
|
+
}
|
|
62
|
+
// npm package names (optionally scoped, optionally with a version/range suffix),
|
|
63
|
+
// simple `--flag` arguments, and pnpm's `--allow-build=<package name>`. Nothing a
|
|
64
|
+
// shell would interpret.
|
|
65
|
+
const SAFE_ARG = /^(?:--[a-z][a-z-]*|--allow-build=(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*|(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-z0-9.^~<>=*_-]+)?|add|install)$/i;
|
|
66
|
+
/**
|
|
67
|
+
* How to spawn a package manager on this platform.
|
|
68
|
+
*
|
|
69
|
+
* On Windows, package managers are `.cmd` shims, and Node refuses to spawn
|
|
70
|
+
* `.cmd`/`.bat` files without a shell (EINVAL since Node 18.20.2 / 20.12.2,
|
|
71
|
+
* CVE-2024-27980). So Windows gets `shell: true` with the bare command name.
|
|
72
|
+
* Arguments are validated against a strict allowlist pattern first, so the
|
|
73
|
+
* shell never sees anything it could interpret.
|
|
74
|
+
*/
|
|
75
|
+
export function packageManagerSpawnSpec(pm, args, platform = process.platform) {
|
|
76
|
+
for (const arg of args) {
|
|
77
|
+
if (!SAFE_ARG.test(arg)) {
|
|
78
|
+
throw new Error(`Refusing to pass unsafe argument to ${pm}: ${JSON.stringify(arg)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (platform === "win32") {
|
|
82
|
+
return { command: pm, args: [...args], shell: true };
|
|
83
|
+
}
|
|
84
|
+
return { command: pm, args: [...args], shell: false };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=package-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../src/package-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builds pnpm must approve to install `@askdb/prisma`, an optional peer (#259):
|
|
3
|
+
* `@prisma/engines` has a postinstall script, and pnpm 11 exits 1 on an unapproved one.
|
|
4
|
+
* Read by guided setup's install plan and the introspection plan's missing-package hint, so the two stay in step. `askdb init`'s copy is `apps/cli/src/prisma-install.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PRISMA_BUILD_APPROVALS: readonly string[];
|
|
7
|
+
//# sourceMappingURL=prisma-install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma-install.d.ts","sourceRoot":"","sources":["../src/prisma-install.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAAwB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builds pnpm must approve to install `@askdb/prisma`, an optional peer (#259):
|
|
3
|
+
* `@prisma/engines` has a postinstall script, and pnpm 11 exits 1 on an unapproved one.
|
|
4
|
+
* Read by guided setup's install plan and the introspection plan's missing-package hint, so the two stay in step. `askdb init`'s copy is `apps/cli/src/prisma-install.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export const PRISMA_BUILD_APPROVALS = ["@prisma/engines"];
|
|
7
|
+
//# sourceMappingURL=prisma-install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma-install.js","sourceRoot":"","sources":["../src/prisma-install.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,CAAC,iBAAiB,CAAC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Request guard for the Studio local server.
|
|
4
|
+
*
|
|
5
|
+
* Studio's API can execute SQL against the configured database and write
|
|
6
|
+
* schema files, so it must not be drivable by arbitrary web pages the
|
|
7
|
+
* developer happens to visit. Three layers, applied before routing:
|
|
8
|
+
*
|
|
9
|
+
* 1. Host allowlist (every request, including static assets) — defeats DNS
|
|
10
|
+
* rebinding: a page on `evil.example` that rebinds to 127.0.0.1 still sends
|
|
11
|
+
* `Host: evil.example:<port>`.
|
|
12
|
+
* 2. Origin + content-type checks on state-changing `/api/*` requests —
|
|
13
|
+
* cross-site `fetch`/form posts carry a foreign `Origin`, and requiring
|
|
14
|
+
* `application/json` forces a CORS preflight that Studio never answers.
|
|
15
|
+
* 3. A per-launch random session token, embedded in the served `index.html`
|
|
16
|
+
* and required on every `/api/*` request as `x-askdb-studio-token`. A
|
|
17
|
+
* cross-origin page can't read `index.html`, so it can't learn the token.
|
|
18
|
+
*/
|
|
19
|
+
export declare const STUDIO_TOKEN_HEADER = "x-askdb-studio-token";
|
|
20
|
+
export declare const STUDIO_TOKEN_META_NAME = "askdb-studio-token";
|
|
21
|
+
export type GuardFailure = {
|
|
22
|
+
status: 403 | 415;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function createStudioSessionToken(): string;
|
|
26
|
+
/** Hosts that mean "listen on every interface". */
|
|
27
|
+
export declare function isWildcardHost(host: string | undefined): boolean;
|
|
28
|
+
/** True for hosts only reachable from this machine. */
|
|
29
|
+
export declare function isLoopbackHost(host: string | undefined): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Host allowlist. Accepts `localhost`, `127.0.0.1`, `[::1]`, and the explicitly
|
|
32
|
+
* configured listen host (or, for wildcard binds, this machine's interface
|
|
33
|
+
* addresses) — and only on the port the connection actually arrived on.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkHost(req: IncomingMessage, listenHost: string | undefined): GuardFailure | null;
|
|
36
|
+
/**
|
|
37
|
+
* `/api/*` checks: same-origin `Origin` on state-changing requests, the
|
|
38
|
+
* per-launch session token, and a JSON content type on requests with a body.
|
|
39
|
+
* Call after {@link checkHost} has passed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkApiRequest(req: IncomingMessage, sessionToken: string): GuardFailure | null;
|
|
42
|
+
/** Insert the session-token `<meta>` tag into the served `index.html`. */
|
|
43
|
+
export declare function injectSessionToken(html: string, sessionToken: string): string;
|
|
44
|
+
//# sourceMappingURL=request-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-guard.d.ts","sourceRoot":"","sources":["../src/request-guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAE3D,MAAM,MAAM,YAAY,GAAG;IAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAElE,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAIhE;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAShE;AAgDD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAenG;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA2B/F;AA4BD,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAG7E"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Request guard for the Studio local server.
|
|
5
|
+
*
|
|
6
|
+
* Studio's API can execute SQL against the configured database and write
|
|
7
|
+
* schema files, so it must not be drivable by arbitrary web pages the
|
|
8
|
+
* developer happens to visit. Three layers, applied before routing:
|
|
9
|
+
*
|
|
10
|
+
* 1. Host allowlist (every request, including static assets) — defeats DNS
|
|
11
|
+
* rebinding: a page on `evil.example` that rebinds to 127.0.0.1 still sends
|
|
12
|
+
* `Host: evil.example:<port>`.
|
|
13
|
+
* 2. Origin + content-type checks on state-changing `/api/*` requests —
|
|
14
|
+
* cross-site `fetch`/form posts carry a foreign `Origin`, and requiring
|
|
15
|
+
* `application/json` forces a CORS preflight that Studio never answers.
|
|
16
|
+
* 3. A per-launch random session token, embedded in the served `index.html`
|
|
17
|
+
* and required on every `/api/*` request as `x-askdb-studio-token`. A
|
|
18
|
+
* cross-origin page can't read `index.html`, so it can't learn the token.
|
|
19
|
+
*/
|
|
20
|
+
export const STUDIO_TOKEN_HEADER = "x-askdb-studio-token";
|
|
21
|
+
export const STUDIO_TOKEN_META_NAME = "askdb-studio-token";
|
|
22
|
+
export function createStudioSessionToken() {
|
|
23
|
+
return randomBytes(32).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
/** Hosts that mean "listen on every interface". */
|
|
26
|
+
export function isWildcardHost(host) {
|
|
27
|
+
if (host === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
const h = host.trim().toLowerCase();
|
|
30
|
+
return h === "" || h === "0.0.0.0" || h === "::" || h === "[::]";
|
|
31
|
+
}
|
|
32
|
+
/** True for hosts only reachable from this machine. */
|
|
33
|
+
export function isLoopbackHost(host) {
|
|
34
|
+
if (host === undefined)
|
|
35
|
+
return true;
|
|
36
|
+
const h = host.trim().toLowerCase();
|
|
37
|
+
return (h === "localhost" ||
|
|
38
|
+
h === "::1" ||
|
|
39
|
+
h === "[::1]" ||
|
|
40
|
+
/^127(?:\.\d{1,3}){3}$/.test(h));
|
|
41
|
+
}
|
|
42
|
+
// A Host header is `hostname[:port]` — nothing else. Rejecting userinfo, paths,
|
|
43
|
+
// and whitespace up front matters because `new URL("http://" + host)` would
|
|
44
|
+
// happily parse `evil@127.0.0.1` as hostname 127.0.0.1.
|
|
45
|
+
const HOST_HEADER_PATTERN = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
|
|
46
|
+
/** Parse a Host header into its normalized `hostname` / `host` / numeric port. */
|
|
47
|
+
function parseHostHeader(value) {
|
|
48
|
+
if (!HOST_HEADER_PATTERN.test(value))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(`http://${value}`);
|
|
52
|
+
return { hostname: url.hostname, host: url.host, port: url.port === "" ? 80 : Number(url.port) };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Normalize a bare host/IP the same way `URL` normalizes a Host header's hostname. */
|
|
59
|
+
function normalizeHostname(host) {
|
|
60
|
+
const trimmed = host.trim();
|
|
61
|
+
if (trimmed === "")
|
|
62
|
+
return null;
|
|
63
|
+
const bracketed = trimmed.includes(":") && !trimmed.startsWith("[") ? `[${trimmed}]` : trimmed;
|
|
64
|
+
try {
|
|
65
|
+
return new URL(`http://${bracketed}`).hostname;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
72
|
+
function isAllowedHostname(hostname, listenHost) {
|
|
73
|
+
if (LOOPBACK_HOSTNAMES.has(hostname))
|
|
74
|
+
return true;
|
|
75
|
+
if (listenHost === undefined)
|
|
76
|
+
return false;
|
|
77
|
+
if (isWildcardHost(listenHost)) {
|
|
78
|
+
// Bound to every interface: accept this machine's own interface addresses
|
|
79
|
+
// (e.g. the LAN IP the CLI prints). IP literals can't be DNS-rebound.
|
|
80
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
81
|
+
for (const entry of entries ?? []) {
|
|
82
|
+
if (normalizeHostname(entry.address) === hostname)
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return normalizeHostname(listenHost) === hostname;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Host allowlist. Accepts `localhost`, `127.0.0.1`, `[::1]`, and the explicitly
|
|
92
|
+
* configured listen host (or, for wildcard binds, this machine's interface
|
|
93
|
+
* addresses) — and only on the port the connection actually arrived on.
|
|
94
|
+
*/
|
|
95
|
+
export function checkHost(req, listenHost) {
|
|
96
|
+
const raw = req.headers.host;
|
|
97
|
+
const parsed = typeof raw === "string" ? parseHostHeader(raw) : null;
|
|
98
|
+
if (!parsed ||
|
|
99
|
+
parsed.port !== req.socket.localPort ||
|
|
100
|
+
!isAllowedHostname(parsed.hostname, listenHost)) {
|
|
101
|
+
return {
|
|
102
|
+
status: 403,
|
|
103
|
+
message: "Studio rejected this request: unrecognized Host header. Open Studio via http://127.0.0.1:<port> or http://localhost:<port>.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
109
|
+
/**
|
|
110
|
+
* `/api/*` checks: same-origin `Origin` on state-changing requests, the
|
|
111
|
+
* per-launch session token, and a JSON content type on requests with a body.
|
|
112
|
+
* Call after {@link checkHost} has passed.
|
|
113
|
+
*/
|
|
114
|
+
export function checkApiRequest(req, sessionToken) {
|
|
115
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
116
|
+
if (!SAFE_METHODS.has(method)) {
|
|
117
|
+
const origin = req.headers.origin;
|
|
118
|
+
if (origin !== undefined && !isSameOrigin(origin, req.headers.host)) {
|
|
119
|
+
return { status: 403, message: "Studio rejected this request: cross-origin requests are not allowed." };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!tokenMatches(req.headers[STUDIO_TOKEN_HEADER], sessionToken)) {
|
|
123
|
+
return {
|
|
124
|
+
status: 403,
|
|
125
|
+
message: "Studio rejected this request: missing or invalid session token. Reload Studio in the browser (the token changes every launch).",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (!SAFE_METHODS.has(method) && (method !== "DELETE" || hasRequestBody(req))) {
|
|
129
|
+
const contentType = req.headers["content-type"];
|
|
130
|
+
const mediaType = typeof contentType === "string" ? contentType.split(";")[0].trim().toLowerCase() : "";
|
|
131
|
+
if (mediaType !== "application/json") {
|
|
132
|
+
return { status: 415, message: "Studio API requests must use Content-Type: application/json." };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function isSameOrigin(origin, hostHeader) {
|
|
138
|
+
if (typeof origin !== "string" || typeof hostHeader !== "string")
|
|
139
|
+
return false;
|
|
140
|
+
const host = parseHostHeader(hostHeader);
|
|
141
|
+
if (!host)
|
|
142
|
+
return false;
|
|
143
|
+
try {
|
|
144
|
+
const url = new URL(origin);
|
|
145
|
+
return url.protocol === "http:" && url.host === host.host;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Includes `Origin: null` (sandboxed iframes, file://, some redirects).
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function tokenMatches(provided, expected) {
|
|
153
|
+
if (typeof provided !== "string")
|
|
154
|
+
return false;
|
|
155
|
+
const a = Buffer.from(provided, "utf8");
|
|
156
|
+
const b = Buffer.from(expected, "utf8");
|
|
157
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
158
|
+
}
|
|
159
|
+
function hasRequestBody(req) {
|
|
160
|
+
const length = req.headers["content-length"];
|
|
161
|
+
if (length !== undefined && Number(length) > 0)
|
|
162
|
+
return true;
|
|
163
|
+
return req.headers["transfer-encoding"] !== undefined;
|
|
164
|
+
}
|
|
165
|
+
/** Insert the session-token `<meta>` tag into the served `index.html`. */
|
|
166
|
+
export function injectSessionToken(html, sessionToken) {
|
|
167
|
+
const tag = `<meta name="${STUDIO_TOKEN_META_NAME}" content="${sessionToken}" />`;
|
|
168
|
+
return html.includes("</head>") ? html.replace("</head>", ` ${tag}\n </head>`) : `${tag}\n${html}`;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=request-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-guard.js","sourceRoot":"","sources":["../src/request-guard.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createPgvectorStore } from "@askdb/rag
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { createPgvectorStore } from "@askdb/rag";
|
|
3
3
|
import { type Workspace } from "@askdb/enrich";
|
|
4
|
-
import type { StudioWorkspaceDto } from "./shared/api.js";
|
|
4
|
+
import type { SetupReason, StudioWorkspaceDto } from "./shared/api.js";
|
|
5
|
+
/** @internal Tests only — serve client assets from another directory. */
|
|
6
|
+
export declare function setStudioClientDirForTests(dir: string | undefined): void;
|
|
5
7
|
export type StudioOptions = {
|
|
6
8
|
schema: string;
|
|
7
9
|
host?: string;
|
|
8
10
|
port?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Start in setup mode instead of failing: `"no-config"` when no
|
|
13
|
+
* `askdb.config.*` was found, `"no-artifact"` when the config exists but the
|
|
14
|
+
* schema artifact hasn't been introspected yet. The browser wizard walks the
|
|
15
|
+
* user through the missing steps and flips the server to ready.
|
|
16
|
+
*/
|
|
17
|
+
setupReason?: SetupReason | null;
|
|
18
|
+
};
|
|
19
|
+
export type StudioServer = Server & {
|
|
20
|
+
/**
|
|
21
|
+
* Per-launch session token. Every `/api/*` request must send it as the
|
|
22
|
+
* `x-askdb-studio-token` header; the browser app reads it from a `<meta>`
|
|
23
|
+
* tag injected into the served `index.html`.
|
|
24
|
+
*/
|
|
25
|
+
readonly sessionToken: string;
|
|
9
26
|
};
|
|
10
|
-
export type StudioServer = ReturnType<typeof createServer>;
|
|
11
27
|
export declare function setStudioPgvectorStoreFactoryForTests(factory: typeof createPgvectorStore | undefined): void;
|
|
12
28
|
export declare function createStudioServer(options: StudioOptions): StudioServer;
|
|
13
29
|
export declare function serializeWorkspace(workspace: Workspace): StudioWorkspaceDto;
|
|
30
|
+
/** Largest JSON request body Studio accepts. */
|
|
31
|
+
export declare const MAX_JSON_BODY_BYTES: number;
|
|
14
32
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAyCjG,OAAO,EAQL,mBAAmB,EAUpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAYV,WAAW,EAKX,kBAAkB,EAGnB,MAAM,iBAAiB,CAAC;AA+BzB,yEAAyE;AACzE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAExE;AAMD,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAClC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAsEF,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAuKvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CA+C3E;AAwsDD,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,QAAc,CAAC"}
|