planche 0.7.1 → 0.7.2

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 (3) hide show
  1. package/README.md +57 -4
  2. package/dist/index.js +38 -6
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -116,6 +116,10 @@ database connections open and forwards queries with the matching driver
116
116
  `~/.ssh/known_hosts` are honoured too). A changed key refuses the
117
117
  connection until the stale entry is removed.
118
118
 
119
+ When `planche.dev` cannot be reached, the agent serves the copy of the app it
120
+ ships with (at its own address) instead of opening a browser error page, so it
121
+ keeps working offline. `--no-serve-app` turns that off.
122
+
119
123
  ## Gateway mode
120
124
 
121
125
  Run one agent on a shared host so a team can use Planche with just a
@@ -132,14 +136,63 @@ npx planche --gateway gateway.json --addr 0.0.0.0 --allow-remote \
132
136
  {
133
137
  "name": "Acme staging",
134
138
  "profiles": [
135
- { "name": "Shop (read-only)", "driver": "mysql", "host": "db", "port": 3306,
136
- "user": "reporting", "password": "…", "database": "shop" },
137
- { "name": "Shop (writable)", "readOnly": false, "driver": "postgres",
138
- "host": "pg", "port": 5432, "user": "app", "password": "…", "database": "shop" }
139
+ {
140
+ "name": "Shop (read-only)",
141
+ "driver": "mysql",
142
+ "host": "db.internal",
143
+ "port": 3306,
144
+ "user": "reporting",
145
+ "password": "…",
146
+ "database": "shop"
147
+ },
148
+ {
149
+ "name": "Shop (writable)",
150
+ "readOnly": false,
151
+ "driver": "postgres",
152
+ "host": "pg.internal",
153
+ "port": 5432,
154
+ "user": "app",
155
+ "password": "…",
156
+ "database": "shop"
157
+ }
139
158
  ]
140
159
  }
141
160
  ```
142
161
 
162
+ `name` is the gateway's own name and `profiles[].name` is what browsers pick;
163
+ every other field is what the connection form asks for, under the same names
164
+ (`serviceName` for Oracle, `file` for SQLite, `accountId` / `databaseId` /
165
+ `apiToken` for D1, and an `ssh` block where you need one).
166
+
167
+ A profile behind a jump host carries that `ssh` block; `host`/`port` are then
168
+ resolved on the jump host, so they are usually loopback:
169
+
170
+ ```json
171
+ {
172
+ "name": "Shop via bastion (read-only)",
173
+ "driver": "mysql",
174
+ "host": "127.0.0.1",
175
+ "port": 3306,
176
+ "user": "reporting",
177
+ "password": "…",
178
+ "database": "shop",
179
+ "ssh": {
180
+ "host": "bastion.example.com",
181
+ "port": 22,
182
+ "user": "deploy",
183
+ "auth": "key"
184
+ }
185
+ }
186
+ ```
187
+
188
+ With `auth: "key"` and no `privateKey`, the agent uses the `IdentityFile` from
189
+ this machine's `~/.ssh/config` (falling back to `~/.ssh/id_*`), so the key
190
+ never lands in the config file; `auth: "agent"` uses `SSH_AUTH_SOCK`. A
191
+ `privateKey` you do set is the key *contents*, not a path. The jump host's key
192
+ must already be in a `known_hosts` file here, or be pinned in the profile with
193
+ `acceptHostKey` — in gateway mode the browser sends only a profile name and
194
+ cannot answer the unknown-host-key prompt.
195
+
143
196
  - Browsers only ever send `{ "profile": "Shop (read-only)" }`; `/connect`
144
197
  bodies with credentials are refused in gateway mode.
145
198
  - Profiles are **read-only by default**: the agent refuses anything but a
package/dist/index.js CHANGED
@@ -2172,6 +2172,29 @@ var DEV_ORIGINS = [
2172
2172
  "http://localhost:4173",
2173
2173
  "http://127.0.0.1:4173"
2174
2174
  ];
2175
+ function bundledAppDir() {
2176
+ const dir = fileURLToPath(new URL("../app/", import.meta.url));
2177
+ return existsSync3(join5(dir, "index.html")) ? dir : null;
2178
+ }
2179
+ function isLoopbackUrl(url) {
2180
+ try {
2181
+ return LOOPBACK_ADDRS.has(new URL(url).hostname);
2182
+ } catch {
2183
+ return false;
2184
+ }
2185
+ }
2186
+ async function appIsReachable(url, timeoutMs = 2500) {
2187
+ const controller = new AbortController();
2188
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2189
+ try {
2190
+ const response = await fetch(url, { signal: controller.signal, redirect: "follow", headers: { "cache-control": "no-store" } });
2191
+ return response.ok;
2192
+ } catch {
2193
+ return false;
2194
+ } finally {
2195
+ clearTimeout(timer);
2196
+ }
2197
+ }
2175
2198
  function resolveAppDir(explicit) {
2176
2199
  const dir = explicit ? resolve3(explicit) : fileURLToPath(new URL("../app/", import.meta.url));
2177
2200
  if (!existsSync3(join5(dir, "index.html"))) {
@@ -2198,8 +2221,8 @@ Options:
2198
2221
  config instead of accepting credentials (env PLANCHE_GATEWAY)
2199
2222
  --public-url <u> URL users open when the agent serves the app itself
2200
2223
  (behind your TLS proxy; env PLANCHE_PUBLIC_URL)
2201
- --no-serve-app Do not serve the web app at / (served by default in
2202
- gateway mode)
2224
+ --no-serve-app Never serve the web app at / (it is served in gateway
2225
+ mode, and when planche.dev cannot be reached)
2203
2226
  --app-dir <dir> Serve this build of the web app instead of the bundled one
2204
2227
  --no-open Do not open the browser
2205
2228
  --verbose Log every request
@@ -2230,7 +2253,7 @@ function openInBrowser(url) {
2230
2253
  } catch {
2231
2254
  }
2232
2255
  }
2233
- function main() {
2256
+ async function main() {
2234
2257
  const major = Number(process.versions.node.split(".")[0]);
2235
2258
  if (major < 22) {
2236
2259
  console.error(`planche needs Node.js 22 or newer (you are running ${process.version}).`);
@@ -2288,10 +2311,13 @@ function main() {
2288
2311
  process.exit(2);
2289
2312
  }
2290
2313
  }
2291
- const serveApp2 = flags["serve-app"] ?? Boolean(gateway);
2314
+ const hostedAppUrl = (flags.app ?? process.env.PLANCHE_APP_URL ?? DEFAULT_APP_URL).replace(/\/+$/, "");
2315
+ let serveApp2 = flags["serve-app"] ?? Boolean(gateway);
2316
+ const offlineFallback = !serveApp2 && flags["serve-app"] !== false && bundledAppDir() !== null && !isLoopbackUrl(hostedAppUrl) && !await appIsReachable(hostedAppUrl);
2317
+ if (offlineFallback) serveApp2 = true;
2292
2318
  const appDir = serveApp2 ? resolveAppDir(flags["app-dir"] ?? process.env.PLANCHE_APP_DIR) : void 0;
2293
2319
  const publicUrl = (flags["public-url"] ?? process.env.PLANCHE_PUBLIC_URL ?? "").replace(/\/+$/, "");
2294
- const appUrl = (serveApp2 ? publicUrl || null : null) ?? (flags.app ?? process.env.PLANCHE_APP_URL ?? DEFAULT_APP_URL).replace(/\/+$/, "");
2320
+ const appUrl = (serveApp2 ? publicUrl || null : null) ?? hostedAppUrl;
2295
2321
  const shouldOpen = flags.open ?? true;
2296
2322
  const bodyLimit = process.env.PLANCHE_BODY_LIMIT || "50mb";
2297
2323
  const appOrigin = originOf(appUrl);
@@ -2341,6 +2367,9 @@ function main() {
2341
2367
  appDir
2342
2368
  });
2343
2369
  const pairUrl = serveApp2 ? `${publicUrl || agentUrl}/#pair=${encodeURIComponent(pairingCode.code)}` : `${appUrl}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(pairingCode.code)}`;
2370
+ if (offlineFallback) {
2371
+ console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
2372
+ }
2344
2373
  const server = app.listen(port, listenAddr, () => {
2345
2374
  const line = "-".repeat(72);
2346
2375
  console.log(line);
@@ -2377,4 +2406,7 @@ function main() {
2377
2406
  process.on("uncaughtException", (err) => console.error("[agent] Uncaught exception:", err instanceof Error ? err.message : String(err)));
2378
2407
  process.on("unhandledRejection", (err) => console.error("[agent] Unhandled rejection:", err instanceof Error ? err.message : String(err)));
2379
2408
  }
2380
- main();
2409
+ main().catch((err) => {
2410
+ console.error(err instanceof Error ? err.message : String(err));
2411
+ process.exit(1);
2412
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "planche",
3
- "version": "0.7.1",
4
- "description": "Local agent for Planche — lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle/Neon/Supabase/Cloudflare D1 databases from your machine.",
3
+ "version": "0.7.2",
4
+ "description": "Local agent for Planche \u2014 lets the browser-based Planche SQL client reach MySQL/MariaDB/PostgreSQL/SQLite/SQL Server/Oracle/Neon/Supabase/Cloudflare D1 databases from your machine.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://planche.dev",
7
7
  "repository": {