planche 0.7.13 → 0.7.14

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 (58) hide show
  1. package/app/assets/{AboutDialog-CxhMAbHE.js → AboutDialog-C2slR8bc.js} +1 -1
  2. package/app/assets/ConnectDialog-C8dt0P44.js +1 -0
  3. package/app/assets/{CopyDatabaseDialog-BwviLOYR.js → CopyDatabaseDialog-koQ5voA2.js} +1 -1
  4. package/app/assets/{CopyTableDialog-y2yVCEoe.js → CopyTableDialog-xXGEdqR9.js} +1 -1
  5. package/app/assets/{CreateDatabaseDialog-v63m43WC.js → CreateDatabaseDialog-DbPAPaHA.js} +1 -1
  6. package/app/assets/{DataDiffDialog-BQvHEwN1.js → DataDiffDialog-BdmXW-yR.js} +1 -1
  7. package/app/assets/{DownloadToCSVDialog-DGQnI5eb.js → DownloadToCSVDialog-DUbNM9Aj.js} +1 -1
  8. package/app/assets/{EditSchemaDialog-DhC0D6ws.js → EditSchemaDialog-B5iWTTUl.js} +1 -1
  9. package/app/assets/{FindReplaceDialog-CluDq2uv.js → FindReplaceDialog-C-w-ut2O.js} +1 -1
  10. package/app/assets/{FlushDialog-DgeG-OGF.js → FlushDialog-EHw9oFc3.js} +1 -1
  11. package/app/assets/{LinkColumnDialog-DBbb55b5.js → LinkColumnDialog-CNRoaG10.js} +1 -1
  12. package/app/assets/{LocalHistoryDialog-BZcsP3oj.js → LocalHistoryDialog-C0tH4J4m.js} +1 -1
  13. package/app/assets/{ProcessListDialog-DpH-Mybn.js → ProcessListDialog-CI60dW_R.js} +1 -1
  14. package/app/assets/{QuickCommandPalette-CjZVdpv1.js → QuickCommandPalette-gFz_jGQW.js} +1 -1
  15. package/app/assets/{RowFormDialog-Cb8MAzWF.js → RowFormDialog-JCW7a_UW.js} +1 -1
  16. package/app/assets/{SafetySettingsDialog-fO9X1PFB.js → SafetySettingsDialog-DGiLgyln.js} +1 -1
  17. package/app/assets/{SchemaDiffDialog-DdRHijgZ.js → SchemaDiffDialog-M9KKdtQR.js} +1 -1
  18. package/app/assets/{SchemaToHTMLDialog-HbVvnUOc.js → SchemaToHTMLDialog-BysAUGf0.js} +1 -1
  19. package/app/assets/{StatusDialog-B2pVC65w.js → StatusDialog-CsIIrMcJ.js} +1 -1
  20. package/app/assets/{TermsDialog-B8Ehf7Ck.js → TermsDialog-BzlyiBiw.js} +1 -1
  21. package/app/assets/{UserAddDialog-CzmcUU7I.js → UserAddDialog-R6QSi55s.js} +1 -1
  22. package/app/assets/{UserManagerDialog-YBOODqbg.js → UserManagerDialog-PgTmxk8V.js} +1 -1
  23. package/app/assets/{VariablesDialog-Dg5CDmWn.js → VariablesDialog-DSdBh3UG.js} +1 -1
  24. package/app/assets/app-storage-C_1RLCyI.js +1 -0
  25. package/app/assets/{connection-store-CAPTzFtT.js → connection-store-DjSajhjh.js} +1 -1
  26. package/app/assets/database-Dvj2X-0B.js +1 -0
  27. package/app/assets/{dbms-Bsjvyvsp.js → dbms-CSyCOszD.js} +1 -1
  28. package/app/assets/de-p3F2-Dhp.js +2 -0
  29. package/app/assets/{engine-DWYEiSZM.js → engine-CzHq4UCB.js} +1 -1
  30. package/app/assets/fr-BghB2Ohx.js +2 -0
  31. package/app/assets/{i18n-NLG6supM.js → i18n-gQOmt8le.js} +2 -2
  32. package/app/assets/ja-O-EyR07G.js +2 -0
  33. package/app/assets/ko-5jmwV68n.js +2 -0
  34. package/app/assets/main-ot9WNZX1.js +940 -0
  35. package/app/assets/pro-usage-BnI7zYsZ.js +1 -0
  36. package/app/assets/{query-execution-BZE0rVpK.js → query-execution-C2muZJbT.js} +3 -3
  37. package/app/assets/{query-store-CRp9AQS5.js → query-store-yK1eqNW-.js} +1 -1
  38. package/app/assets/{schema-nodes-D3-cz2Xk.js → schema-nodes-2owuMIuw.js} +1 -1
  39. package/app/assets/{select-DAm5iaPc.js → select-DlF-eYgr.js} +1 -1
  40. package/app/assets/{settings-store-CYHWBgNo.js → settings-store-CSRx4hwh.js} +1 -1
  41. package/app/assets/{table-copy-93YWKupe.js → table-copy-B1tGampL.js} +1 -1
  42. package/app/assets/use-engine-CwOU9o8-.js +1 -0
  43. package/app/assets/zh-BIU7z3hl.js +2 -0
  44. package/app/index.html +18 -16
  45. package/app/landing.css +13 -6
  46. package/dist/index.js +525 -22
  47. package/package.json +9 -4
  48. package/app/assets/ConnectDialog-Dk5tee_2.js +0 -1
  49. package/app/assets/app-storage-B6v72zLH.js +0 -1
  50. package/app/assets/database-ezjCHdX9.js +0 -1
  51. package/app/assets/de-DP-iF2aJ.js +0 -2
  52. package/app/assets/fr-DFtpsn6X.js +0 -2
  53. package/app/assets/ja--An7okvc.js +0 -2
  54. package/app/assets/ko-C-OjtONK.js +0 -2
  55. package/app/assets/main-Cs_AF7Zo.js +0 -940
  56. package/app/assets/pro-usage-VlZJ74Cl.js +0 -1
  57. package/app/assets/use-engine-DSWrApw6.js +0 -1
  58. package/app/assets/zh-C4dUMxzb.js +0 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { randomUUID as randomUUID2 } from "crypto";
4
+ import { randomUUID as randomUUID3 } from "crypto";
5
5
  import { createRequire as createRequire2 } from "module";
6
6
  import { spawn } from "child_process";
7
7
  import { parseArgs } from "util";
@@ -1028,6 +1028,468 @@ var d1DriverFactory = {
1028
1028
  }
1029
1029
  };
1030
1030
 
1031
+ // src/drivers/do.ts
1032
+ var LOCAL_EXPLORER = "/cdn-cgi/local/explorer/api";
1033
+ var CLOUDFLARE_API = "https://api.cloudflare.com/client/v4";
1034
+ var MAX_OBJECTS = 5e3;
1035
+ var DoError = class extends Error {
1036
+ constructor(message, status) {
1037
+ super(message);
1038
+ this.status = status;
1039
+ }
1040
+ status;
1041
+ };
1042
+ var DATABASE_LIST2 = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
1043
+ var RESULT_KIND2 = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
1044
+ var TRANSACTION = /^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
1045
+ var DDL = /^\s*(CREATE|ALTER|DROP)\b/i;
1046
+ var HEX_ID = /^[0-9a-f]{64}$/i;
1047
+ function singleSourceTable3(sql2) {
1048
+ const m = /\bFROM\s+("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
1049
+ if (!m) return null;
1050
+ const rest = m[2].replace(
1051
+ /\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i,
1052
+ ""
1053
+ );
1054
+ if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
1055
+ const raw = m[1];
1056
+ const first = raw[0];
1057
+ if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
1058
+ if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
1059
+ if (first === "[") return raw.slice(1, -1);
1060
+ return raw;
1061
+ }
1062
+ function normalizeDeclaredType3(declared) {
1063
+ if (typeof declared !== "string") return null;
1064
+ const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
1065
+ return base || null;
1066
+ }
1067
+ function inferTypeFromValue3(value) {
1068
+ if (value === null || value === void 0) return "null";
1069
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
1070
+ if (Array.isArray(value)) return "blob";
1071
+ return "text";
1072
+ }
1073
+ function firstNonNull3(records, index) {
1074
+ for (const row of records) {
1075
+ const value = row[index];
1076
+ if (value !== null && value !== void 0) return value;
1077
+ }
1078
+ return null;
1079
+ }
1080
+ function closingQuote(sql2, start, close) {
1081
+ let i = start + 1;
1082
+ while (i < sql2.length) {
1083
+ if (sql2[i] === close) {
1084
+ if (sql2[i + 1] === close && close !== "]") {
1085
+ i += 2;
1086
+ continue;
1087
+ }
1088
+ return i + 1;
1089
+ }
1090
+ i += 1;
1091
+ }
1092
+ return sql2.length;
1093
+ }
1094
+ function skipSpace(sql2, from) {
1095
+ let i = from;
1096
+ while (i < sql2.length && /\s/.test(sql2[i])) i += 1;
1097
+ return i;
1098
+ }
1099
+ function routeStatement(sql2, known) {
1100
+ let out = "";
1101
+ let target = null;
1102
+ let i = 0;
1103
+ while (i < sql2.length) {
1104
+ const ch = sql2[i];
1105
+ if (ch === "'") {
1106
+ const end = closingQuote(sql2, i, "'");
1107
+ out += sql2.slice(i, end);
1108
+ i = end;
1109
+ continue;
1110
+ }
1111
+ if (ch === "-" && sql2[i + 1] === "-") {
1112
+ const end = sql2.indexOf("\n", i);
1113
+ const stop = end === -1 ? sql2.length : end;
1114
+ out += sql2.slice(i, stop);
1115
+ i = stop;
1116
+ continue;
1117
+ }
1118
+ if (ch === "/" && sql2[i + 1] === "*") {
1119
+ const end = sql2.indexOf("*/", i + 2);
1120
+ const stop = end === -1 ? sql2.length : end + 2;
1121
+ out += sql2.slice(i, stop);
1122
+ i = stop;
1123
+ continue;
1124
+ }
1125
+ if (ch === '"' || ch === "`" || ch === "[") {
1126
+ const close = ch === "[" ? "]" : ch;
1127
+ const end = closingQuote(sql2, i, close);
1128
+ const raw = sql2.slice(i, end);
1129
+ const inner = ch === "[" ? raw.slice(1, -1) : raw.slice(1, -1).replaceAll(close + close, close);
1130
+ const dot = skipSpace(sql2, end);
1131
+ if (sql2[dot] === "." && known(inner)) {
1132
+ target ??= inner;
1133
+ i = dot + 1;
1134
+ continue;
1135
+ }
1136
+ out += raw;
1137
+ i = end;
1138
+ continue;
1139
+ }
1140
+ out += ch;
1141
+ i += 1;
1142
+ }
1143
+ return { sql: out, target };
1144
+ }
1145
+ async function fetchJson(url, init, unreachable) {
1146
+ let res;
1147
+ try {
1148
+ res = await fetch(url, { ...init, signal: AbortSignal.timeout(6e4) });
1149
+ } catch (err) {
1150
+ throw unreachable(err instanceof Error ? err.message : String(err));
1151
+ }
1152
+ let body = null;
1153
+ try {
1154
+ body = await res.json();
1155
+ } catch {
1156
+ }
1157
+ return { res, body };
1158
+ }
1159
+ var LocalTransport = class {
1160
+ label;
1161
+ base;
1162
+ constructor(devUrl) {
1163
+ this.label = `do:${devUrl}`;
1164
+ this.base = `${devUrl.replace(/\/+$/, "")}${LOCAL_EXPLORER}`;
1165
+ }
1166
+ async call(path, body) {
1167
+ const origin = this.base.replace(LOCAL_EXPLORER, "");
1168
+ const { res, body: parsed } = await fetchJson(
1169
+ `${this.base}${path}`,
1170
+ {
1171
+ method: body === void 0 ? "GET" : "POST",
1172
+ headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
1173
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1174
+ },
1175
+ (reason) => new DoError(
1176
+ `Cannot reach a wrangler dev server at ${origin} (${reason}). Run \`wrangler dev\` in your Worker project and try again.`,
1177
+ 400
1178
+ )
1179
+ );
1180
+ const envelope = parsed && typeof parsed.success === "boolean" && (parsed.result !== void 0 || Array.isArray(parsed.errors)) ? parsed : null;
1181
+ if (!envelope) {
1182
+ throw new DoError(
1183
+ `${origin} answered (HTTP ${res.status}) but is not a wrangler dev server. Check the port, and that \`wrangler dev\` (wrangler 4.x) is what is running there.`,
1184
+ 400
1185
+ );
1186
+ }
1187
+ if (!res.ok || !envelope.success) {
1188
+ throw new DoError(errorText(envelope.errors) || `wrangler dev responded ${res.status}`, res.ok ? void 0 : 400);
1189
+ }
1190
+ if (envelope.result === void 0) throw new DoError("wrangler dev returned no result");
1191
+ return envelope.result;
1192
+ }
1193
+ async namespaces() {
1194
+ const list = await this.call("/workers/durable_objects/namespaces");
1195
+ return list.map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1196
+ }
1197
+ objects(namespaceId) {
1198
+ return this.call(`/workers/durable_objects/namespaces/${encodeURIComponent(namespaceId)}/objects`);
1199
+ }
1200
+ async run(target, sql2) {
1201
+ const selector = target.name ? { durable_object_name: target.name } : { durable_object_id: target.id };
1202
+ const results = await this.call(
1203
+ `/workers/durable_objects/namespaces/${encodeURIComponent(target.namespaceId)}/query`,
1204
+ { ...selector, queries: [{ sql: sql2 }] }
1205
+ );
1206
+ const result = results[0];
1207
+ if (!result) throw new DoError("wrangler dev returned no result");
1208
+ return result;
1209
+ }
1210
+ };
1211
+ var RemoteTransport = class {
1212
+ label;
1213
+ base;
1214
+ headers;
1215
+ only;
1216
+ constructor(config) {
1217
+ this.label = `do:${config.accountId}${config.namespaceId ? `/${config.namespaceId}` : ""}`;
1218
+ this.base = `${CLOUDFLARE_API}/accounts/${encodeURIComponent(config.accountId)}/workers/durable_objects/namespaces`;
1219
+ this.headers = { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" };
1220
+ this.only = config.namespaceId || void 0;
1221
+ }
1222
+ async call(path, body) {
1223
+ const { res, body: parsed } = await fetchJson(
1224
+ `${this.base}${path}`,
1225
+ {
1226
+ method: body === void 0 ? "GET" : "POST",
1227
+ headers: this.headers,
1228
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1229
+ },
1230
+ (reason) => new DoError(`Cannot reach the Cloudflare API: ${reason}`, 400)
1231
+ );
1232
+ if (res.status === 401 || res.status === 403) {
1233
+ throw new DoError(
1234
+ errorText(parsed?.errors) || "Cloudflare refused the API token. It needs the Workers Scripts Read permission on this account.",
1235
+ 401
1236
+ );
1237
+ }
1238
+ if (res.status === 404 || res.status === 405) {
1239
+ throw new DoError(
1240
+ "Cloudflare did not accept this request (HTTP " + res.status + "). The Durable Object query endpoint is unofficial and may have changed; the Data Studio tab in the Cloudflare dashboard still works.",
1241
+ 400
1242
+ );
1243
+ }
1244
+ if (!res.ok || !parsed?.success) {
1245
+ throw new DoError(errorText(parsed?.errors) || `Cloudflare API responded ${res.status}`, 400);
1246
+ }
1247
+ return parsed;
1248
+ }
1249
+ async namespaces() {
1250
+ if (this.only) return [{ id: this.only, sqlite: true }];
1251
+ const body = await this.call("");
1252
+ return (body.result ?? []).map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1253
+ }
1254
+ /**
1255
+ * Cloudflare's listing returns ids and `hasStoredData`, never names. It also
1256
+ * answers an empty page for small `limit` values, so the parameter is left
1257
+ * off and the default page size is used, following `result_info.cursor`.
1258
+ */
1259
+ async objects(namespaceId) {
1260
+ const out = [];
1261
+ let cursor;
1262
+ do {
1263
+ const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
1264
+ const body = await this.call(
1265
+ `/${encodeURIComponent(namespaceId)}/objects${query}`
1266
+ );
1267
+ for (const object of body.result ?? []) out.push({ id: object.id });
1268
+ cursor = body.result_info?.cursor || void 0;
1269
+ } while (cursor && out.length < MAX_OBJECTS);
1270
+ return out;
1271
+ }
1272
+ async run(target, sql2) {
1273
+ if (!target.id) {
1274
+ throw new DoError(
1275
+ `"${target.name}" is a name, and deployed Durable Objects can only be opened by id: idFromName() is a one-way hash and Cloudflare does not resolve it here. Pick an object from the schema tree.`,
1276
+ 400
1277
+ );
1278
+ }
1279
+ const body = await this.call(
1280
+ `/${encodeURIComponent(target.namespaceId)}/query`,
1281
+ { durable_object_id: target.id, queries: [{ sql: sql2 }] }
1282
+ );
1283
+ const result = body.result?.results?.[0];
1284
+ if (!result) throw new DoError("Cloudflare returned no result");
1285
+ return result;
1286
+ }
1287
+ };
1288
+ function errorText(errors) {
1289
+ if (!Array.isArray(errors)) return "";
1290
+ return errors.map((e) => typeof e === "string" ? e : String(e?.message ?? "")).filter(Boolean).join("; ").slice(0, 500);
1291
+ }
1292
+ var DoDriver = class {
1293
+ constructor(type, transport) {
1294
+ this.type = type;
1295
+ this.transport = transport;
1296
+ this.label = transport.label;
1297
+ }
1298
+ type;
1299
+ transport;
1300
+ label;
1301
+ /** Database name -> the object it addresses, refreshed by every listing. */
1302
+ targets = /* @__PURE__ */ new Map();
1303
+ /** Declared column types per `database table`, for result-source resolution. */
1304
+ columns = /* @__PURE__ */ new Map();
1305
+ /**
1306
+ * Every object of every SQLite-backed namespace, as app-level database
1307
+ * names. Locally, a `wrangler dev` session registers itself in wrangler's
1308
+ * dev registry, so sessions of other projects show up here too.
1309
+ */
1310
+ async listDatabases() {
1311
+ const namespaces = await this.transport.namespaces();
1312
+ const names = [];
1313
+ this.targets.clear();
1314
+ for (const ns of namespaces) {
1315
+ if (!ns.sqlite) continue;
1316
+ let objects;
1317
+ try {
1318
+ objects = await this.transport.objects(ns.id);
1319
+ } catch {
1320
+ continue;
1321
+ }
1322
+ for (const object of objects) {
1323
+ const ref = object.name || object.id;
1324
+ if (!ref) continue;
1325
+ const database = `${ns.id}/${ref}`;
1326
+ this.targets.set(database, { namespaceId: ns.id, name: object.name, id: object.id });
1327
+ names.push(database);
1328
+ }
1329
+ }
1330
+ return names.sort();
1331
+ }
1332
+ /**
1333
+ * Resolve a database name to the object it addresses. Names are cached by
1334
+ * the last listing; one that was never listed is still usable, so a saved
1335
+ * query tab keeps working — the trailing part is read as an id when it looks
1336
+ * like one and as a name otherwise.
1337
+ */
1338
+ targetFor(database) {
1339
+ const cached2 = this.targets.get(database);
1340
+ if (cached2) return cached2;
1341
+ const slash = database.indexOf("/");
1342
+ if (slash <= 0 || slash === database.length - 1) {
1343
+ throw new DoError(
1344
+ `"${database}" is not a Durable Object. Pick one from the schema tree (they are named "<namespace>/<object>").`,
1345
+ 400
1346
+ );
1347
+ }
1348
+ const namespaceId = database.slice(0, slash);
1349
+ const ref = database.slice(slash + 1);
1350
+ return HEX_ID.test(ref) ? { namespaceId, id: ref } : { namespaceId, name: ref };
1351
+ }
1352
+ async tableColumns(database, target, table) {
1353
+ const key = `${database} ${table}`;
1354
+ const cached2 = this.columns.get(key);
1355
+ if (cached2 !== void 0) return cached2;
1356
+ let out = null;
1357
+ try {
1358
+ const info = await this.transport.run(target, `PRAGMA table_info("${table.replaceAll('"', '""')}")`);
1359
+ const cols = info.columns ?? [];
1360
+ const name = cols.indexOf("name");
1361
+ const type = cols.indexOf("type");
1362
+ const rows = info.rows ?? [];
1363
+ if (rows.length > 0 && name >= 0) {
1364
+ out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType3(type >= 0 ? r[type] : null)]));
1365
+ }
1366
+ } catch {
1367
+ out = null;
1368
+ }
1369
+ if (this.columns.size > 200) this.columns.clear();
1370
+ this.columns.set(key, out);
1371
+ return out;
1372
+ }
1373
+ async query(sql2, database) {
1374
+ if (DATABASE_LIST2.test(sql2)) {
1375
+ const names = await this.listDatabases();
1376
+ return {
1377
+ fields: ["seq", "name", "file"].map((name2) => ({
1378
+ name: name2,
1379
+ orgName: name2,
1380
+ type: name2 === "seq" ? "integer" : "text",
1381
+ table: "",
1382
+ orgTable: "",
1383
+ db: "",
1384
+ maxLength: 0
1385
+ })),
1386
+ records: names.map((name2, seq) => [seq, name2, ""]),
1387
+ affectedRows: 0,
1388
+ insertId: 0,
1389
+ isResultQuery: true
1390
+ };
1391
+ }
1392
+ const routed = routeStatement(sql2, (name2) => this.targets.has(name2) || name2.includes("/"));
1393
+ const text = routed.sql;
1394
+ if (TRANSACTION.test(text)) {
1395
+ throw new DoError(
1396
+ "Durable Object storage does not take transaction statements over this API; every request auto-commits."
1397
+ );
1398
+ }
1399
+ const name = routed.target ?? database;
1400
+ if (!name) {
1401
+ throw new DoError(
1402
+ 'No Durable Object selected. Pick one in the schema tree, or qualify the table as "<namespace>/<object>"."<table>".',
1403
+ 400
1404
+ );
1405
+ }
1406
+ const target = this.targetFor(name);
1407
+ if (DDL.test(text)) this.columns.clear();
1408
+ const result = await this.transport.run(target, text);
1409
+ const columns = result.columns ?? [];
1410
+ const records = result.rows ?? [];
1411
+ if (columns.length === 0 && !RESULT_KIND2.test(text)) {
1412
+ return {
1413
+ fields: [],
1414
+ records: [],
1415
+ affectedRows: result.meta?.rows_written ?? 0,
1416
+ insertId: 0,
1417
+ isResultQuery: false
1418
+ };
1419
+ }
1420
+ const source = RESULT_KIND2.test(text) ? singleSourceTable3(text) : null;
1421
+ const known = source ? await this.tableColumns(name, target, source) : null;
1422
+ const fields = columns.map((column, index) => {
1423
+ const fromTable = known?.has(column) ?? false;
1424
+ const type = (fromTable ? known?.get(column) : null) ?? inferTypeFromValue3(firstNonNull3(records, index));
1425
+ return {
1426
+ name: column,
1427
+ orgName: column,
1428
+ type,
1429
+ table: fromTable && source ? source : "",
1430
+ orgTable: fromTable && source ? source : "",
1431
+ db: fromTable ? name : "",
1432
+ maxLength: 0
1433
+ };
1434
+ });
1435
+ return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
1436
+ }
1437
+ async close() {
1438
+ this.targets.clear();
1439
+ this.columns.clear();
1440
+ }
1441
+ };
1442
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
1443
+ function parseDevUrl(raw) {
1444
+ let url;
1445
+ try {
1446
+ url = new URL(raw);
1447
+ } catch {
1448
+ throw new Error(`Not a valid URL: ${raw}`);
1449
+ }
1450
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1451
+ throw new Error("devUrl must be an http(s) URL");
1452
+ }
1453
+ if (!LOOPBACK.has(url.hostname.toLowerCase())) {
1454
+ throw new Error(
1455
+ "devUrl must point at a wrangler dev server on this machine (localhost). Use the deployed Durable Objects connection for objects that are not local."
1456
+ );
1457
+ }
1458
+ return url.origin;
1459
+ }
1460
+ var doDriverFactory = {
1461
+ async open(config) {
1462
+ const driver = new DoDriver("do", new LocalTransport(config.devUrl));
1463
+ await driver.query("PRAGMA database_list");
1464
+ return driver;
1465
+ },
1466
+ async test(config) {
1467
+ await new DoDriver("do", new LocalTransport(config.devUrl)).query("PRAGMA database_list");
1468
+ },
1469
+ parseConfig(body) {
1470
+ const devUrl = typeof body.devUrl === "string" ? body.devUrl.trim() : "";
1471
+ if (!devUrl) throw new Error("devUrl is required");
1472
+ return { driver: "do", devUrl: parseDevUrl(devUrl) };
1473
+ }
1474
+ };
1475
+ var doRemoteDriverFactory = {
1476
+ async open(config) {
1477
+ const driver = new DoDriver("do-remote", new RemoteTransport(config));
1478
+ await driver.query("PRAGMA database_list");
1479
+ return driver;
1480
+ },
1481
+ async test(config) {
1482
+ await new DoDriver("do-remote", new RemoteTransport(config)).query("PRAGMA database_list");
1483
+ },
1484
+ parseConfig(body) {
1485
+ const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
1486
+ const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
1487
+ const namespaceId = typeof body.namespaceId === "string" ? body.namespaceId.trim() : "";
1488
+ if (!accountId || !apiToken) throw new Error("accountId and apiToken are required");
1489
+ return { driver: "do-remote", accountId, apiToken, ...namespaceId ? { namespaceId } : {} };
1490
+ }
1491
+ };
1492
+
1031
1493
  // src/ssh-tunnel.ts
1032
1494
  import { createServer } from "net";
1033
1495
  import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
@@ -1551,23 +2013,28 @@ var factories = {
1551
2013
  sqlite: sqliteDriverFactory,
1552
2014
  mssql: mssqlDriverFactory,
1553
2015
  oracle: oracleDriverFactory,
1554
- d1: d1DriverFactory
2016
+ d1: d1DriverFactory,
2017
+ do: doDriverFactory,
2018
+ "do-remote": doRemoteDriverFactory
1555
2019
  };
1556
2020
  function isDriverType(value) {
1557
2021
  return typeof value === "string" && value in factories;
1558
2022
  }
2023
+ function isDirectDriver(config) {
2024
+ return config.driver === "sqlite" || config.driver === "d1" || config.driver === "do" || config.driver === "do-remote";
2025
+ }
1559
2026
  function parseDriverConfig(body) {
1560
2027
  const driver = body.driver ?? "mysql";
1561
2028
  if (!isDriverType(driver)) {
1562
2029
  throw new Error(`Unsupported driver: ${String(driver)}`);
1563
2030
  }
1564
2031
  const config = factories[driver].parseConfig(body);
1565
- if (config.driver === "sqlite" || config.driver === "d1") return config;
2032
+ if (isDirectDriver(config)) return config;
1566
2033
  const ssh = parseSshConfig(body.ssh);
1567
2034
  return ssh ? { ...config, ssh } : config;
1568
2035
  }
1569
2036
  async function withTunnel(config, run) {
1570
- if (config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return run(config, void 0);
2037
+ if (isDirectDriver(config) || !config.ssh) return run(config, void 0);
1571
2038
  const tunnel = await openSshTunnel(config.ssh, { host: config.host, port: config.port });
1572
2039
  recordUsage("ssh_tunnel");
1573
2040
  try {
@@ -1595,7 +2062,7 @@ function tunneled(driver, tunnel, label) {
1595
2062
  function openDriver(config) {
1596
2063
  return withTunnel(config, async (direct, tunnel) => {
1597
2064
  const driver = await openDirect(direct);
1598
- if (!tunnel || config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return driver;
2065
+ if (!tunnel || isDirectDriver(config) || !config.ssh) return driver;
1599
2066
  const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
1600
2067
  return tunneled(driver, tunnel, label);
1601
2068
  });
@@ -1623,6 +2090,10 @@ function openDirect(config) {
1623
2090
  return factories.oracle.open(config);
1624
2091
  case "d1":
1625
2092
  return factories.d1.open(config);
2093
+ case "do":
2094
+ return factories.do.open(config);
2095
+ case "do-remote":
2096
+ return factories["do-remote"].open(config);
1626
2097
  }
1627
2098
  }
1628
2099
  function testDirect(config) {
@@ -1639,6 +2110,10 @@ function testDirect(config) {
1639
2110
  return factories.oracle.test(config);
1640
2111
  case "d1":
1641
2112
  return factories.d1.test(config);
2113
+ case "do":
2114
+ return factories.do.test(config);
2115
+ case "do-remote":
2116
+ return factories["do-remote"].test(config);
1642
2117
  }
1643
2118
  }
1644
2119
 
@@ -1886,7 +2361,7 @@ async function updateNotice(current) {
1886
2361
 
1887
2362
  // src/server.ts
1888
2363
  import express from "express";
1889
- import { createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "crypto";
2364
+ import { createHash as createHash2, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1890
2365
 
1891
2366
  // src/tunneling.ts
1892
2367
  import { randomUUID } from "crypto";
@@ -2253,7 +2728,9 @@ function createAgent(options) {
2253
2728
  const allowedOrigins = new Set(options.allowedOrigins.map((o) => o.replace(/\/+$/, "")));
2254
2729
  const allowedHosts = options.allowedHosts ? new Set(options.allowedHosts.map((h) => h.toLowerCase())) : null;
2255
2730
  const throttle = new AuthThrottle();
2731
+ const pairThrottle = new AuthThrottle();
2256
2732
  let pairing = options.pairingCode;
2733
+ let lastPairOffer = 0;
2257
2734
  app.set("env", "production");
2258
2735
  app.disable("x-powered-by");
2259
2736
  app.disable("etag");
@@ -2288,6 +2765,19 @@ function createAgent(options) {
2288
2765
  });
2289
2766
  app.use(accessControl(allowedOrigins));
2290
2767
  if (options.appDir) serveApp(app, options.appDir);
2768
+ const PAIR_OFFER_INTERVAL_MS = 6e4;
2769
+ function offerPairing(req) {
2770
+ if (!options.onUnpaired) return false;
2771
+ const origin = req.headers.origin;
2772
+ if (typeof origin !== "string" || !allowedOrigins.has(origin.replace(/\/+$/, ""))) return false;
2773
+ const now = Date.now();
2774
+ if (pairing && now <= pairing.expiresAt) return false;
2775
+ if (now - lastPairOffer < PAIR_OFFER_INTERVAL_MS) return false;
2776
+ lastPairOffer = now;
2777
+ pairing = { code: randomUUID2(), expiresAt: now + 10 * 60 * 1e3 };
2778
+ options.onUnpaired(pairing, origin);
2779
+ return true;
2780
+ }
2291
2781
  app.get("/health", (req, res) => {
2292
2782
  const body = {
2293
2783
  success: true,
@@ -2300,23 +2790,21 @@ function createAgent(options) {
2300
2790
  if (options.install) body.install = options.install;
2301
2791
  if (options.gateway) body.gateway = { name: options.gateway.name, profiles: options.gateway.profiles.length };
2302
2792
  if (req.headers.authorization !== void 0) {
2303
- if (throttle.locked(peerOf(req))) {
2304
- fail(res, 429, "Too many failed authentication attempts; try again shortly");
2305
- return;
2306
- }
2307
2793
  body.authorized = authorized(req);
2308
2794
  }
2795
+ if (body.authorized !== true) void offerPairing(req);
2309
2796
  res.json(body);
2310
2797
  });
2311
2798
  app.post("/pair", express.json({ limit: "1kb" }), (req, res) => {
2312
- if (throttle.locked(peerOf(req))) {
2313
- fail(res, 429, "Too many failed authentication attempts; try again shortly");
2799
+ if (pairThrottle.locked(peerOf(req))) {
2800
+ fail(res, 429, "Too many failed pairing attempts; try again shortly");
2314
2801
  return;
2315
2802
  }
2316
2803
  const code = typeof req.body?.code === "string" ? req.body.code : "";
2317
2804
  if (!pairing || Date.now() > pairing.expiresAt || !code || !secretEquals(code, pairing.code)) {
2318
- throttle.fail(peerOf(req));
2319
- fail(res, 401, "Pairing code is invalid or has expired; restart the agent for a new link");
2805
+ pairThrottle.fail(peerOf(req));
2806
+ const printed = offerPairing(req);
2807
+ fail(res, 401, printed ? "Pairing code is invalid or has expired; a fresh link has been printed in the agent's terminal" : "Pairing code is invalid or has expired; open the pairing link shown in the agent's terminal");
2320
2808
  return;
2321
2809
  }
2322
2810
  pairing = void 0;
@@ -2633,7 +3121,7 @@ async function main() {
2633
3121
  const port = Number(flags.port ?? process.env.PLANCHE_PORT) || 8888;
2634
3122
  const addr = flags.addr ?? process.env.PLANCHE_ADDR ?? "127.0.0.1";
2635
3123
  const tokenFromEnv = !flags.token && !!process.env.PLANCHE_AUTH_TOKEN;
2636
- const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID2();
3124
+ const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID3();
2637
3125
  const gatewayFile = flags.gateway ?? process.env.PLANCHE_GATEWAY;
2638
3126
  let gateway;
2639
3127
  if (gatewayFile) {
@@ -2684,9 +3172,11 @@ async function main() {
2684
3172
  const devApp = LOOPBACK_ADDRS.has(new URL(appUrl).hostname);
2685
3173
  const allowedOrigins = [appOrigin, ...devApp ? DEV_ORIGINS : [], ...extraOrigins];
2686
3174
  if (serveApp2) allowedOrigins.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`);
2687
- const pairingCode = { code: randomUUID2(), expiresAt: Date.now() + 10 * 60 * 1e3 };
3175
+ const pairingCode = { code: randomUUID3(), expiresAt: Date.now() + 10 * 60 * 1e3 };
2688
3176
  const listenAddr = addr.replace(/^\[(.*)\]$/, "$1");
2689
3177
  const agentUrl = `http://${listenAddr.includes(":") ? `[${listenAddr}]` : listenAddr}:${port}`;
3178
+ const divider = "-".repeat(72);
3179
+ const pairLink = (code, appBase) => serveApp2 ? `${appBase}/#pair=${encodeURIComponent(code)}` : `${appBase}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(code)}`;
2690
3180
  const { app, manager } = createAgent({
2691
3181
  authToken,
2692
3182
  allowedOrigins,
@@ -2698,17 +3188,30 @@ async function main() {
2698
3188
  allowedHosts: loopback ? ["127.0.0.1", "localhost", "[::1]", `127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`] : void 0,
2699
3189
  pairingCode,
2700
3190
  gateway,
2701
- appDir
3191
+ appDir,
3192
+ // An app that cannot authenticate gets a fresh link on this terminal
3193
+ // rather than needing the agent restarted. The link is built from the
3194
+ // origin the request came from, so it points at the app that is really
3195
+ // running — a dev server bumped to another port included.
3196
+ onUnpaired: (fresh, appOrigin2) => {
3197
+ const url = pairLink(fresh.code, serveApp2 ? publicUrl || appOrigin2 : appOrigin2);
3198
+ console.log(divider);
3199
+ console.log(`The app at ${appOrigin2} is not paired with this agent.`);
3200
+ console.log(`Pair it with (valid for 10 minutes, single use):
3201
+
3202
+ ${url}
3203
+ `);
3204
+ console.log(divider);
3205
+ }
2702
3206
  });
2703
- const pairUrl = serveApp2 ? `${publicUrl || agentUrl}/#pair=${encodeURIComponent(pairingCode.code)}` : `${appUrl}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(pairingCode.code)}`;
3207
+ const pairUrl = pairLink(pairingCode.code, serveApp2 ? publicUrl || agentUrl : appUrl);
2704
3208
  if (offlineFallback) {
2705
3209
  console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
2706
3210
  }
2707
3211
  const server = app.listen(port, listenAddr, () => {
2708
- const line = "-".repeat(72);
2709
- console.log(line);
3212
+ console.log(divider);
2710
3213
  console.log(`Planche agent ${VERSION} listening on ${agentUrl}`);
2711
- console.log(line);
3214
+ console.log(divider);
2712
3215
  console.log(`Open the app with (link valid for 10 minutes, single use):
2713
3216
 
2714
3217
  ${pairUrl}
@@ -2719,7 +3222,7 @@ async function main() {
2719
3222
  if (!loopback) {
2720
3223
  console.error(`WARNING: bound to ${addr} without TLS \u2014 anyone who can reach this port and knows the token can use your databases, and anyone on the path can read the traffic.`);
2721
3224
  }
2722
- console.log(line);
3225
+ console.log(divider);
2723
3226
  if (shouldOpen) openInBrowser(pairUrl);
2724
3227
  void updateNotice(VERSION).then((notice) => {
2725
3228
  if (notice) console.log(notice);