planche 0.7.13 → 0.7.15
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/app/assets/{AboutDialog-CxhMAbHE.js → AboutDialog-DJ1xTjQa.js} +1 -1
- package/app/assets/ConnectDialog-CsyQZTfv.js +1 -0
- package/app/assets/CopyDatabaseDialog-DVaHAD1q.js +1 -0
- package/app/assets/{CopyTableDialog-y2yVCEoe.js → CopyTableDialog-BGaxEeFf.js} +1 -1
- package/app/assets/CreateDatabaseDialog-DiwtxqWI.js +1 -0
- package/app/assets/CreateSchemaDialog-CPhHOdED.js +1 -0
- package/app/assets/{DataDiffDialog-BQvHEwN1.js → DataDiffDialog-BQzfzN_-.js} +1 -1
- package/app/assets/DownloadToCSVDialog-CMQ_k6oK.js +3 -0
- package/app/assets/EditSchemaDialog-e9w9yyIn.js +2 -0
- package/app/assets/FindReplaceDialog-BbbQS-3c.js +1 -0
- package/app/assets/{FlushDialog-DgeG-OGF.js → FlushDialog-D_tK375i.js} +1 -1
- package/app/assets/{LinkColumnDialog-DBbb55b5.js → LinkColumnDialog-Df-_8K5T.js} +1 -1
- package/app/assets/{LocalHistoryDialog-BZcsP3oj.js → LocalHistoryDialog-B3Yww-nk.js} +1 -1
- package/app/assets/{ProcessListDialog-DpH-Mybn.js → ProcessListDialog-6YJn364S.js} +1 -1
- package/app/assets/{QuickCommandPalette-CjZVdpv1.js → QuickCommandPalette-Cvvri_sm.js} +1 -1
- package/app/assets/{RowFormDialog-Cb8MAzWF.js → RowFormDialog-B5yd3Kv7.js} +1 -1
- package/app/assets/{SafetySettingsDialog-fO9X1PFB.js → SafetySettingsDialog-ce5eqfIE.js} +1 -1
- package/app/assets/{SchemaDiffDialog-DdRHijgZ.js → SchemaDiffDialog-CAU4Tko1.js} +1 -1
- package/app/assets/{SchemaToHTMLDialog-HbVvnUOc.js → SchemaToHTMLDialog-Bp8NQ4S0.js} +1 -1
- package/app/assets/{StatusDialog-B2pVC65w.js → StatusDialog-CT8E2pK0.js} +1 -1
- package/app/assets/{TermsDialog-B8Ehf7Ck.js → TermsDialog-BzlyiBiw.js} +1 -1
- package/app/assets/{UserAddDialog-CzmcUU7I.js → UserAddDialog-CyK4YAvW.js} +1 -1
- package/app/assets/UserManagerDialog-oRNIMr07.js +1 -0
- package/app/assets/{VariablesDialog-Dg5CDmWn.js → VariablesDialog-bHfrRqC3.js} +1 -1
- package/app/assets/app-storage-C_1RLCyI.js +1 -0
- package/app/assets/{connection-store-CAPTzFtT.js → connection-store-DjSajhjh.js} +1 -1
- package/app/assets/database-BUN6Jvbi.js +1 -0
- package/app/assets/{dbms-Bsjvyvsp.js → dbms-B9Zk7nEm.js} +8 -8
- package/app/assets/de-sebDJePc.js +2 -0
- package/app/assets/editor-registry-C_yarsyY.js +1 -0
- package/app/assets/{engine-DWYEiSZM.js → engine-C2ZJalx5.js} +1 -1
- package/app/assets/fr-CIPqitfo.js +2 -0
- package/app/assets/hmr-singleton-Bit0Kfog.js +1 -0
- package/app/assets/{i18n-NLG6supM.js → i18n-BBdlx4Gw.js} +2 -2
- package/app/assets/ja-Bf1tvh4M.js +2 -0
- package/app/assets/ko-D8httUGO.js +2 -0
- package/app/assets/main-BjGObGVl.js +940 -0
- package/app/assets/main-DaPLBcIe.css +2 -0
- package/app/assets/pro-usage-dBNmC2gt.js +1 -0
- package/app/assets/query-execution-DqpMGs8z.js +7 -0
- package/app/assets/{query-store-CRp9AQS5.js → query-store-Bz1pmp4E.js} +1 -1
- package/app/assets/{schema-nodes-D3-cz2Xk.js → schema-nodes-XgnUaB2x.js} +1 -1
- package/app/assets/{select-DAm5iaPc.js → select-BFa0--sE.js} +1 -1
- package/app/assets/{settings-store-CYHWBgNo.js → settings-store-CSRx4hwh.js} +1 -1
- package/app/assets/table-copy-W19HZGKM.js +1 -0
- package/app/assets/use-engine-C_C2qpfD.js +1 -0
- package/app/assets/zh-DAWEX6ju.js +2 -0
- package/app/index.html +21 -17
- package/app/landing.css +13 -6
- package/dist/index.js +611 -23
- package/package.json +9 -4
- package/app/assets/ConnectDialog-Dk5tee_2.js +0 -1
- package/app/assets/CopyDatabaseDialog-BwviLOYR.js +0 -1
- package/app/assets/CreateDatabaseDialog-v63m43WC.js +0 -1
- package/app/assets/DownloadToCSVDialog-DGQnI5eb.js +0 -3
- package/app/assets/EditSchemaDialog-DhC0D6ws.js +0 -1
- package/app/assets/FindReplaceDialog-CluDq2uv.js +0 -1
- package/app/assets/UserManagerDialog-YBOODqbg.js +0 -1
- package/app/assets/app-storage-B6v72zLH.js +0 -1
- package/app/assets/database-ezjCHdX9.js +0 -1
- package/app/assets/de-DP-iF2aJ.js +0 -2
- package/app/assets/fr-DFtpsn6X.js +0 -2
- package/app/assets/ja--An7okvc.js +0 -2
- package/app/assets/ko-C-OjtONK.js +0 -2
- package/app/assets/main-Cs_AF7Zo.js +0 -940
- package/app/assets/main-pmntwMQg.css +0 -2
- package/app/assets/pro-usage-VlZJ74Cl.js +0 -1
- package/app/assets/query-execution-BZE0rVpK.js +0 -7
- package/app/assets/table-copy-93YWKupe.js +0 -1
- package/app/assets/use-engine-DSWrApw6.js +0 -1
- 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
|
|
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";
|
|
@@ -61,10 +61,12 @@ var USE_STATEMENT = /^\s*USE\s/i;
|
|
|
61
61
|
var MySqlDriver = class {
|
|
62
62
|
constructor(connection, config) {
|
|
63
63
|
this.connection = connection;
|
|
64
|
+
this.config = config;
|
|
64
65
|
this.label = `${config.user}@${config.host}:${config.port}`;
|
|
65
66
|
this.currentDatabase = config.database ?? null;
|
|
66
67
|
}
|
|
67
68
|
connection;
|
|
69
|
+
config;
|
|
68
70
|
type = "mysql";
|
|
69
71
|
label;
|
|
70
72
|
/**
|
|
@@ -110,6 +112,21 @@ var MySqlDriver = class {
|
|
|
110
112
|
async setReadOnly() {
|
|
111
113
|
await this.connection.query("SET SESSION TRANSACTION READ ONLY");
|
|
112
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* A connection running a statement cannot take another one, so the KILL has
|
|
117
|
+
* to come down a second connection. `KILL QUERY` ends the statement and
|
|
118
|
+
* leaves the session — and its open transaction — alone.
|
|
119
|
+
*/
|
|
120
|
+
async cancel() {
|
|
121
|
+
const threadId = this.connection.threadId;
|
|
122
|
+
if (!threadId) return;
|
|
123
|
+
const killer = await mysql.createConnection(toConnectionOptions(this.config));
|
|
124
|
+
try {
|
|
125
|
+
await killer.query(`KILL QUERY ${Number(threadId)}`);
|
|
126
|
+
} finally {
|
|
127
|
+
await killer.end().catch(() => void 0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
async close() {
|
|
114
131
|
await this.connection.end();
|
|
115
132
|
}
|
|
@@ -219,9 +236,11 @@ var quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
|
219
236
|
var PostgresDriver = class {
|
|
220
237
|
constructor(client, config) {
|
|
221
238
|
this.client = client;
|
|
239
|
+
this.config = config;
|
|
222
240
|
this.label = `${config.user}@${config.host}:${config.port}/${config.database}`;
|
|
223
241
|
}
|
|
224
242
|
client;
|
|
243
|
+
config;
|
|
225
244
|
type = "postgres";
|
|
226
245
|
label;
|
|
227
246
|
/**
|
|
@@ -306,6 +325,21 @@ var PostgresDriver = class {
|
|
|
306
325
|
async setReadOnly() {
|
|
307
326
|
await this.client.query("SET default_transaction_read_only = on");
|
|
308
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* `pg_cancel_backend` from a second connection: the busy one cannot carry the
|
|
330
|
+
* request. It cancels the running statement only, leaving the session open.
|
|
331
|
+
*/
|
|
332
|
+
async cancel() {
|
|
333
|
+
const pid = this.client.processID;
|
|
334
|
+
if (!pid) return;
|
|
335
|
+
const canceller = new pg.Client(toClientConfig(this.config));
|
|
336
|
+
try {
|
|
337
|
+
await canceller.connect();
|
|
338
|
+
await canceller.query("SELECT pg_cancel_backend($1)", [pid]);
|
|
339
|
+
} finally {
|
|
340
|
+
await canceller.end().catch(() => void 0);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
309
343
|
async close() {
|
|
310
344
|
await this.client.end();
|
|
311
345
|
}
|
|
@@ -641,10 +675,22 @@ var MssqlDriver = class {
|
|
|
641
675
|
this.sources.set(text, result);
|
|
642
676
|
return result;
|
|
643
677
|
}
|
|
678
|
+
/** The request in flight, so a cancel can interrupt it. */
|
|
679
|
+
running = null;
|
|
680
|
+
/** `tedious` sends an attention packet; the statement ends and the pool stays usable. */
|
|
681
|
+
async cancel() {
|
|
682
|
+
this.running?.cancel();
|
|
683
|
+
}
|
|
644
684
|
async query(text) {
|
|
645
685
|
const req = this.pool.request();
|
|
646
686
|
req.arrayRowMode = true;
|
|
647
|
-
|
|
687
|
+
this.running = req;
|
|
688
|
+
let res;
|
|
689
|
+
try {
|
|
690
|
+
res = await req.query(text);
|
|
691
|
+
} finally {
|
|
692
|
+
if (this.running === req) this.running = null;
|
|
693
|
+
}
|
|
648
694
|
const sets = res.recordsets;
|
|
649
695
|
const last = sets.length > 0 ? sets[sets.length - 1] : void 0;
|
|
650
696
|
const columns = last?.columns;
|
|
@@ -817,6 +863,14 @@ var OracleDriver = class {
|
|
|
817
863
|
}
|
|
818
864
|
return { fields: [], records: [], affectedRows: res.rowsAffected ?? 0, insertId: 0, isResultQuery: false };
|
|
819
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* `break()` interrupts the call in progress; the session then needs
|
|
868
|
+
* `reset()` before it can be used again.
|
|
869
|
+
*/
|
|
870
|
+
async cancel() {
|
|
871
|
+
await this.conn.break();
|
|
872
|
+
await this.conn.reset?.().catch(() => void 0);
|
|
873
|
+
}
|
|
820
874
|
async close() {
|
|
821
875
|
await this.conn.close();
|
|
822
876
|
}
|
|
@@ -1028,6 +1082,468 @@ var d1DriverFactory = {
|
|
|
1028
1082
|
}
|
|
1029
1083
|
};
|
|
1030
1084
|
|
|
1085
|
+
// src/drivers/do.ts
|
|
1086
|
+
var LOCAL_EXPLORER = "/cdn-cgi/local/explorer/api";
|
|
1087
|
+
var CLOUDFLARE_API = "https://api.cloudflare.com/client/v4";
|
|
1088
|
+
var MAX_OBJECTS = 5e3;
|
|
1089
|
+
var DoError = class extends Error {
|
|
1090
|
+
constructor(message, status) {
|
|
1091
|
+
super(message);
|
|
1092
|
+
this.status = status;
|
|
1093
|
+
}
|
|
1094
|
+
status;
|
|
1095
|
+
};
|
|
1096
|
+
var DATABASE_LIST2 = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
|
|
1097
|
+
var RESULT_KIND2 = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
|
|
1098
|
+
var TRANSACTION = /^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
|
|
1099
|
+
var DDL = /^\s*(CREATE|ALTER|DROP)\b/i;
|
|
1100
|
+
var HEX_ID = /^[0-9a-f]{64}$/i;
|
|
1101
|
+
function singleSourceTable3(sql2) {
|
|
1102
|
+
const m = /\bFROM\s+("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
|
|
1103
|
+
if (!m) return null;
|
|
1104
|
+
const rest = m[2].replace(
|
|
1105
|
+
/\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i,
|
|
1106
|
+
""
|
|
1107
|
+
);
|
|
1108
|
+
if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
|
|
1109
|
+
const raw = m[1];
|
|
1110
|
+
const first = raw[0];
|
|
1111
|
+
if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
|
|
1112
|
+
if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
|
|
1113
|
+
if (first === "[") return raw.slice(1, -1);
|
|
1114
|
+
return raw;
|
|
1115
|
+
}
|
|
1116
|
+
function normalizeDeclaredType3(declared) {
|
|
1117
|
+
if (typeof declared !== "string") return null;
|
|
1118
|
+
const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
|
|
1119
|
+
return base || null;
|
|
1120
|
+
}
|
|
1121
|
+
function inferTypeFromValue3(value) {
|
|
1122
|
+
if (value === null || value === void 0) return "null";
|
|
1123
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
1124
|
+
if (Array.isArray(value)) return "blob";
|
|
1125
|
+
return "text";
|
|
1126
|
+
}
|
|
1127
|
+
function firstNonNull3(records, index) {
|
|
1128
|
+
for (const row of records) {
|
|
1129
|
+
const value = row[index];
|
|
1130
|
+
if (value !== null && value !== void 0) return value;
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
function closingQuote(sql2, start, close) {
|
|
1135
|
+
let i = start + 1;
|
|
1136
|
+
while (i < sql2.length) {
|
|
1137
|
+
if (sql2[i] === close) {
|
|
1138
|
+
if (sql2[i + 1] === close && close !== "]") {
|
|
1139
|
+
i += 2;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
return i + 1;
|
|
1143
|
+
}
|
|
1144
|
+
i += 1;
|
|
1145
|
+
}
|
|
1146
|
+
return sql2.length;
|
|
1147
|
+
}
|
|
1148
|
+
function skipSpace(sql2, from) {
|
|
1149
|
+
let i = from;
|
|
1150
|
+
while (i < sql2.length && /\s/.test(sql2[i])) i += 1;
|
|
1151
|
+
return i;
|
|
1152
|
+
}
|
|
1153
|
+
function routeStatement(sql2, known) {
|
|
1154
|
+
let out = "";
|
|
1155
|
+
let target = null;
|
|
1156
|
+
let i = 0;
|
|
1157
|
+
while (i < sql2.length) {
|
|
1158
|
+
const ch = sql2[i];
|
|
1159
|
+
if (ch === "'") {
|
|
1160
|
+
const end = closingQuote(sql2, i, "'");
|
|
1161
|
+
out += sql2.slice(i, end);
|
|
1162
|
+
i = end;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (ch === "-" && sql2[i + 1] === "-") {
|
|
1166
|
+
const end = sql2.indexOf("\n", i);
|
|
1167
|
+
const stop = end === -1 ? sql2.length : end;
|
|
1168
|
+
out += sql2.slice(i, stop);
|
|
1169
|
+
i = stop;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (ch === "/" && sql2[i + 1] === "*") {
|
|
1173
|
+
const end = sql2.indexOf("*/", i + 2);
|
|
1174
|
+
const stop = end === -1 ? sql2.length : end + 2;
|
|
1175
|
+
out += sql2.slice(i, stop);
|
|
1176
|
+
i = stop;
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (ch === '"' || ch === "`" || ch === "[") {
|
|
1180
|
+
const close = ch === "[" ? "]" : ch;
|
|
1181
|
+
const end = closingQuote(sql2, i, close);
|
|
1182
|
+
const raw = sql2.slice(i, end);
|
|
1183
|
+
const inner = ch === "[" ? raw.slice(1, -1) : raw.slice(1, -1).replaceAll(close + close, close);
|
|
1184
|
+
const dot = skipSpace(sql2, end);
|
|
1185
|
+
if (sql2[dot] === "." && known(inner)) {
|
|
1186
|
+
target ??= inner;
|
|
1187
|
+
i = dot + 1;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
out += raw;
|
|
1191
|
+
i = end;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
out += ch;
|
|
1195
|
+
i += 1;
|
|
1196
|
+
}
|
|
1197
|
+
return { sql: out, target };
|
|
1198
|
+
}
|
|
1199
|
+
async function fetchJson(url, init, unreachable) {
|
|
1200
|
+
let res;
|
|
1201
|
+
try {
|
|
1202
|
+
res = await fetch(url, { ...init, signal: AbortSignal.timeout(6e4) });
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
throw unreachable(err instanceof Error ? err.message : String(err));
|
|
1205
|
+
}
|
|
1206
|
+
let body = null;
|
|
1207
|
+
try {
|
|
1208
|
+
body = await res.json();
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
return { res, body };
|
|
1212
|
+
}
|
|
1213
|
+
var LocalTransport = class {
|
|
1214
|
+
label;
|
|
1215
|
+
base;
|
|
1216
|
+
constructor(devUrl) {
|
|
1217
|
+
this.label = `do:${devUrl}`;
|
|
1218
|
+
this.base = `${devUrl.replace(/\/+$/, "")}${LOCAL_EXPLORER}`;
|
|
1219
|
+
}
|
|
1220
|
+
async call(path, body) {
|
|
1221
|
+
const origin = this.base.replace(LOCAL_EXPLORER, "");
|
|
1222
|
+
const { res, body: parsed } = await fetchJson(
|
|
1223
|
+
`${this.base}${path}`,
|
|
1224
|
+
{
|
|
1225
|
+
method: body === void 0 ? "GET" : "POST",
|
|
1226
|
+
headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
|
|
1227
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1228
|
+
},
|
|
1229
|
+
(reason) => new DoError(
|
|
1230
|
+
`Cannot reach a wrangler dev server at ${origin} (${reason}). Run \`wrangler dev\` in your Worker project and try again.`,
|
|
1231
|
+
400
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
const envelope = parsed && typeof parsed.success === "boolean" && (parsed.result !== void 0 || Array.isArray(parsed.errors)) ? parsed : null;
|
|
1235
|
+
if (!envelope) {
|
|
1236
|
+
throw new DoError(
|
|
1237
|
+
`${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.`,
|
|
1238
|
+
400
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (!res.ok || !envelope.success) {
|
|
1242
|
+
throw new DoError(errorText(envelope.errors) || `wrangler dev responded ${res.status}`, res.ok ? void 0 : 400);
|
|
1243
|
+
}
|
|
1244
|
+
if (envelope.result === void 0) throw new DoError("wrangler dev returned no result");
|
|
1245
|
+
return envelope.result;
|
|
1246
|
+
}
|
|
1247
|
+
async namespaces() {
|
|
1248
|
+
const list = await this.call("/workers/durable_objects/namespaces");
|
|
1249
|
+
return list.map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
|
|
1250
|
+
}
|
|
1251
|
+
objects(namespaceId) {
|
|
1252
|
+
return this.call(`/workers/durable_objects/namespaces/${encodeURIComponent(namespaceId)}/objects`);
|
|
1253
|
+
}
|
|
1254
|
+
async run(target, sql2) {
|
|
1255
|
+
const selector = target.name ? { durable_object_name: target.name } : { durable_object_id: target.id };
|
|
1256
|
+
const results = await this.call(
|
|
1257
|
+
`/workers/durable_objects/namespaces/${encodeURIComponent(target.namespaceId)}/query`,
|
|
1258
|
+
{ ...selector, queries: [{ sql: sql2 }] }
|
|
1259
|
+
);
|
|
1260
|
+
const result = results[0];
|
|
1261
|
+
if (!result) throw new DoError("wrangler dev returned no result");
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
var RemoteTransport = class {
|
|
1266
|
+
label;
|
|
1267
|
+
base;
|
|
1268
|
+
headers;
|
|
1269
|
+
only;
|
|
1270
|
+
constructor(config) {
|
|
1271
|
+
this.label = `do:${config.accountId}${config.namespaceId ? `/${config.namespaceId}` : ""}`;
|
|
1272
|
+
this.base = `${CLOUDFLARE_API}/accounts/${encodeURIComponent(config.accountId)}/workers/durable_objects/namespaces`;
|
|
1273
|
+
this.headers = { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" };
|
|
1274
|
+
this.only = config.namespaceId || void 0;
|
|
1275
|
+
}
|
|
1276
|
+
async call(path, body) {
|
|
1277
|
+
const { res, body: parsed } = await fetchJson(
|
|
1278
|
+
`${this.base}${path}`,
|
|
1279
|
+
{
|
|
1280
|
+
method: body === void 0 ? "GET" : "POST",
|
|
1281
|
+
headers: this.headers,
|
|
1282
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1283
|
+
},
|
|
1284
|
+
(reason) => new DoError(`Cannot reach the Cloudflare API: ${reason}`, 400)
|
|
1285
|
+
);
|
|
1286
|
+
if (res.status === 401 || res.status === 403) {
|
|
1287
|
+
throw new DoError(
|
|
1288
|
+
errorText(parsed?.errors) || "Cloudflare refused the API token. It needs the Workers Scripts Read permission on this account.",
|
|
1289
|
+
401
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (res.status === 404 || res.status === 405) {
|
|
1293
|
+
throw new DoError(
|
|
1294
|
+
"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.",
|
|
1295
|
+
400
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (!res.ok || !parsed?.success) {
|
|
1299
|
+
throw new DoError(errorText(parsed?.errors) || `Cloudflare API responded ${res.status}`, 400);
|
|
1300
|
+
}
|
|
1301
|
+
return parsed;
|
|
1302
|
+
}
|
|
1303
|
+
async namespaces() {
|
|
1304
|
+
if (this.only) return [{ id: this.only, sqlite: true }];
|
|
1305
|
+
const body = await this.call("");
|
|
1306
|
+
return (body.result ?? []).map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Cloudflare's listing returns ids and `hasStoredData`, never names. It also
|
|
1310
|
+
* answers an empty page for small `limit` values, so the parameter is left
|
|
1311
|
+
* off and the default page size is used, following `result_info.cursor`.
|
|
1312
|
+
*/
|
|
1313
|
+
async objects(namespaceId) {
|
|
1314
|
+
const out = [];
|
|
1315
|
+
let cursor;
|
|
1316
|
+
do {
|
|
1317
|
+
const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
|
|
1318
|
+
const body = await this.call(
|
|
1319
|
+
`/${encodeURIComponent(namespaceId)}/objects${query}`
|
|
1320
|
+
);
|
|
1321
|
+
for (const object of body.result ?? []) out.push({ id: object.id });
|
|
1322
|
+
cursor = body.result_info?.cursor || void 0;
|
|
1323
|
+
} while (cursor && out.length < MAX_OBJECTS);
|
|
1324
|
+
return out;
|
|
1325
|
+
}
|
|
1326
|
+
async run(target, sql2) {
|
|
1327
|
+
if (!target.id) {
|
|
1328
|
+
throw new DoError(
|
|
1329
|
+
`"${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.`,
|
|
1330
|
+
400
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const body = await this.call(
|
|
1334
|
+
`/${encodeURIComponent(target.namespaceId)}/query`,
|
|
1335
|
+
{ durable_object_id: target.id, queries: [{ sql: sql2 }] }
|
|
1336
|
+
);
|
|
1337
|
+
const result = body.result?.results?.[0];
|
|
1338
|
+
if (!result) throw new DoError("Cloudflare returned no result");
|
|
1339
|
+
return result;
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
function errorText(errors) {
|
|
1343
|
+
if (!Array.isArray(errors)) return "";
|
|
1344
|
+
return errors.map((e) => typeof e === "string" ? e : String(e?.message ?? "")).filter(Boolean).join("; ").slice(0, 500);
|
|
1345
|
+
}
|
|
1346
|
+
var DoDriver = class {
|
|
1347
|
+
constructor(type, transport) {
|
|
1348
|
+
this.type = type;
|
|
1349
|
+
this.transport = transport;
|
|
1350
|
+
this.label = transport.label;
|
|
1351
|
+
}
|
|
1352
|
+
type;
|
|
1353
|
+
transport;
|
|
1354
|
+
label;
|
|
1355
|
+
/** Database name -> the object it addresses, refreshed by every listing. */
|
|
1356
|
+
targets = /* @__PURE__ */ new Map();
|
|
1357
|
+
/** Declared column types per `database table`, for result-source resolution. */
|
|
1358
|
+
columns = /* @__PURE__ */ new Map();
|
|
1359
|
+
/**
|
|
1360
|
+
* Every object of every SQLite-backed namespace, as app-level database
|
|
1361
|
+
* names. Locally, a `wrangler dev` session registers itself in wrangler's
|
|
1362
|
+
* dev registry, so sessions of other projects show up here too.
|
|
1363
|
+
*/
|
|
1364
|
+
async listDatabases() {
|
|
1365
|
+
const namespaces = await this.transport.namespaces();
|
|
1366
|
+
const names = [];
|
|
1367
|
+
this.targets.clear();
|
|
1368
|
+
for (const ns of namespaces) {
|
|
1369
|
+
if (!ns.sqlite) continue;
|
|
1370
|
+
let objects;
|
|
1371
|
+
try {
|
|
1372
|
+
objects = await this.transport.objects(ns.id);
|
|
1373
|
+
} catch {
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
for (const object of objects) {
|
|
1377
|
+
const ref = object.name || object.id;
|
|
1378
|
+
if (!ref) continue;
|
|
1379
|
+
const database = `${ns.id}/${ref}`;
|
|
1380
|
+
this.targets.set(database, { namespaceId: ns.id, name: object.name, id: object.id });
|
|
1381
|
+
names.push(database);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return names.sort();
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Resolve a database name to the object it addresses. Names are cached by
|
|
1388
|
+
* the last listing; one that was never listed is still usable, so a saved
|
|
1389
|
+
* query tab keeps working — the trailing part is read as an id when it looks
|
|
1390
|
+
* like one and as a name otherwise.
|
|
1391
|
+
*/
|
|
1392
|
+
targetFor(database) {
|
|
1393
|
+
const cached2 = this.targets.get(database);
|
|
1394
|
+
if (cached2) return cached2;
|
|
1395
|
+
const slash = database.indexOf("/");
|
|
1396
|
+
if (slash <= 0 || slash === database.length - 1) {
|
|
1397
|
+
throw new DoError(
|
|
1398
|
+
`"${database}" is not a Durable Object. Pick one from the schema tree (they are named "<namespace>/<object>").`,
|
|
1399
|
+
400
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const namespaceId = database.slice(0, slash);
|
|
1403
|
+
const ref = database.slice(slash + 1);
|
|
1404
|
+
return HEX_ID.test(ref) ? { namespaceId, id: ref } : { namespaceId, name: ref };
|
|
1405
|
+
}
|
|
1406
|
+
async tableColumns(database, target, table) {
|
|
1407
|
+
const key = `${database} ${table}`;
|
|
1408
|
+
const cached2 = this.columns.get(key);
|
|
1409
|
+
if (cached2 !== void 0) return cached2;
|
|
1410
|
+
let out = null;
|
|
1411
|
+
try {
|
|
1412
|
+
const info = await this.transport.run(target, `PRAGMA table_info("${table.replaceAll('"', '""')}")`);
|
|
1413
|
+
const cols = info.columns ?? [];
|
|
1414
|
+
const name = cols.indexOf("name");
|
|
1415
|
+
const type = cols.indexOf("type");
|
|
1416
|
+
const rows = info.rows ?? [];
|
|
1417
|
+
if (rows.length > 0 && name >= 0) {
|
|
1418
|
+
out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType3(type >= 0 ? r[type] : null)]));
|
|
1419
|
+
}
|
|
1420
|
+
} catch {
|
|
1421
|
+
out = null;
|
|
1422
|
+
}
|
|
1423
|
+
if (this.columns.size > 200) this.columns.clear();
|
|
1424
|
+
this.columns.set(key, out);
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1427
|
+
async query(sql2, database) {
|
|
1428
|
+
if (DATABASE_LIST2.test(sql2)) {
|
|
1429
|
+
const names = await this.listDatabases();
|
|
1430
|
+
return {
|
|
1431
|
+
fields: ["seq", "name", "file"].map((name2) => ({
|
|
1432
|
+
name: name2,
|
|
1433
|
+
orgName: name2,
|
|
1434
|
+
type: name2 === "seq" ? "integer" : "text",
|
|
1435
|
+
table: "",
|
|
1436
|
+
orgTable: "",
|
|
1437
|
+
db: "",
|
|
1438
|
+
maxLength: 0
|
|
1439
|
+
})),
|
|
1440
|
+
records: names.map((name2, seq) => [seq, name2, ""]),
|
|
1441
|
+
affectedRows: 0,
|
|
1442
|
+
insertId: 0,
|
|
1443
|
+
isResultQuery: true
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
const routed = routeStatement(sql2, (name2) => this.targets.has(name2) || name2.includes("/"));
|
|
1447
|
+
const text = routed.sql;
|
|
1448
|
+
if (TRANSACTION.test(text)) {
|
|
1449
|
+
throw new DoError(
|
|
1450
|
+
"Durable Object storage does not take transaction statements over this API; every request auto-commits."
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
const name = routed.target ?? database;
|
|
1454
|
+
if (!name) {
|
|
1455
|
+
throw new DoError(
|
|
1456
|
+
'No Durable Object selected. Pick one in the schema tree, or qualify the table as "<namespace>/<object>"."<table>".',
|
|
1457
|
+
400
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const target = this.targetFor(name);
|
|
1461
|
+
if (DDL.test(text)) this.columns.clear();
|
|
1462
|
+
const result = await this.transport.run(target, text);
|
|
1463
|
+
const columns = result.columns ?? [];
|
|
1464
|
+
const records = result.rows ?? [];
|
|
1465
|
+
if (columns.length === 0 && !RESULT_KIND2.test(text)) {
|
|
1466
|
+
return {
|
|
1467
|
+
fields: [],
|
|
1468
|
+
records: [],
|
|
1469
|
+
affectedRows: result.meta?.rows_written ?? 0,
|
|
1470
|
+
insertId: 0,
|
|
1471
|
+
isResultQuery: false
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
const source = RESULT_KIND2.test(text) ? singleSourceTable3(text) : null;
|
|
1475
|
+
const known = source ? await this.tableColumns(name, target, source) : null;
|
|
1476
|
+
const fields = columns.map((column, index) => {
|
|
1477
|
+
const fromTable = known?.has(column) ?? false;
|
|
1478
|
+
const type = (fromTable ? known?.get(column) : null) ?? inferTypeFromValue3(firstNonNull3(records, index));
|
|
1479
|
+
return {
|
|
1480
|
+
name: column,
|
|
1481
|
+
orgName: column,
|
|
1482
|
+
type,
|
|
1483
|
+
table: fromTable && source ? source : "",
|
|
1484
|
+
orgTable: fromTable && source ? source : "",
|
|
1485
|
+
db: fromTable ? name : "",
|
|
1486
|
+
maxLength: 0
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1489
|
+
return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
|
|
1490
|
+
}
|
|
1491
|
+
async close() {
|
|
1492
|
+
this.targets.clear();
|
|
1493
|
+
this.columns.clear();
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
|
|
1497
|
+
function parseDevUrl(raw) {
|
|
1498
|
+
let url;
|
|
1499
|
+
try {
|
|
1500
|
+
url = new URL(raw);
|
|
1501
|
+
} catch {
|
|
1502
|
+
throw new Error(`Not a valid URL: ${raw}`);
|
|
1503
|
+
}
|
|
1504
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1505
|
+
throw new Error("devUrl must be an http(s) URL");
|
|
1506
|
+
}
|
|
1507
|
+
if (!LOOPBACK.has(url.hostname.toLowerCase())) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
"devUrl must point at a wrangler dev server on this machine (localhost). Use the deployed Durable Objects connection for objects that are not local."
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
return url.origin;
|
|
1513
|
+
}
|
|
1514
|
+
var doDriverFactory = {
|
|
1515
|
+
async open(config) {
|
|
1516
|
+
const driver = new DoDriver("do", new LocalTransport(config.devUrl));
|
|
1517
|
+
await driver.query("PRAGMA database_list");
|
|
1518
|
+
return driver;
|
|
1519
|
+
},
|
|
1520
|
+
async test(config) {
|
|
1521
|
+
await new DoDriver("do", new LocalTransport(config.devUrl)).query("PRAGMA database_list");
|
|
1522
|
+
},
|
|
1523
|
+
parseConfig(body) {
|
|
1524
|
+
const devUrl = typeof body.devUrl === "string" ? body.devUrl.trim() : "";
|
|
1525
|
+
if (!devUrl) throw new Error("devUrl is required");
|
|
1526
|
+
return { driver: "do", devUrl: parseDevUrl(devUrl) };
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
var doRemoteDriverFactory = {
|
|
1530
|
+
async open(config) {
|
|
1531
|
+
const driver = new DoDriver("do-remote", new RemoteTransport(config));
|
|
1532
|
+
await driver.query("PRAGMA database_list");
|
|
1533
|
+
return driver;
|
|
1534
|
+
},
|
|
1535
|
+
async test(config) {
|
|
1536
|
+
await new DoDriver("do-remote", new RemoteTransport(config)).query("PRAGMA database_list");
|
|
1537
|
+
},
|
|
1538
|
+
parseConfig(body) {
|
|
1539
|
+
const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
|
|
1540
|
+
const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
|
|
1541
|
+
const namespaceId = typeof body.namespaceId === "string" ? body.namespaceId.trim() : "";
|
|
1542
|
+
if (!accountId || !apiToken) throw new Error("accountId and apiToken are required");
|
|
1543
|
+
return { driver: "do-remote", accountId, apiToken, ...namespaceId ? { namespaceId } : {} };
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1031
1547
|
// src/ssh-tunnel.ts
|
|
1032
1548
|
import { createServer } from "net";
|
|
1033
1549
|
import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
|
|
@@ -1551,23 +2067,28 @@ var factories = {
|
|
|
1551
2067
|
sqlite: sqliteDriverFactory,
|
|
1552
2068
|
mssql: mssqlDriverFactory,
|
|
1553
2069
|
oracle: oracleDriverFactory,
|
|
1554
|
-
d1: d1DriverFactory
|
|
2070
|
+
d1: d1DriverFactory,
|
|
2071
|
+
do: doDriverFactory,
|
|
2072
|
+
"do-remote": doRemoteDriverFactory
|
|
1555
2073
|
};
|
|
1556
2074
|
function isDriverType(value) {
|
|
1557
2075
|
return typeof value === "string" && value in factories;
|
|
1558
2076
|
}
|
|
2077
|
+
function isDirectDriver(config) {
|
|
2078
|
+
return config.driver === "sqlite" || config.driver === "d1" || config.driver === "do" || config.driver === "do-remote";
|
|
2079
|
+
}
|
|
1559
2080
|
function parseDriverConfig(body) {
|
|
1560
2081
|
const driver = body.driver ?? "mysql";
|
|
1561
2082
|
if (!isDriverType(driver)) {
|
|
1562
2083
|
throw new Error(`Unsupported driver: ${String(driver)}`);
|
|
1563
2084
|
}
|
|
1564
2085
|
const config = factories[driver].parseConfig(body);
|
|
1565
|
-
if (config
|
|
2086
|
+
if (isDirectDriver(config)) return config;
|
|
1566
2087
|
const ssh = parseSshConfig(body.ssh);
|
|
1567
2088
|
return ssh ? { ...config, ssh } : config;
|
|
1568
2089
|
}
|
|
1569
2090
|
async function withTunnel(config, run) {
|
|
1570
|
-
if (config
|
|
2091
|
+
if (isDirectDriver(config) || !config.ssh) return run(config, void 0);
|
|
1571
2092
|
const tunnel = await openSshTunnel(config.ssh, { host: config.host, port: config.port });
|
|
1572
2093
|
recordUsage("ssh_tunnel");
|
|
1573
2094
|
try {
|
|
@@ -1595,7 +2116,7 @@ function tunneled(driver, tunnel, label) {
|
|
|
1595
2116
|
function openDriver(config) {
|
|
1596
2117
|
return withTunnel(config, async (direct, tunnel) => {
|
|
1597
2118
|
const driver = await openDirect(direct);
|
|
1598
|
-
if (!tunnel || config
|
|
2119
|
+
if (!tunnel || isDirectDriver(config) || !config.ssh) return driver;
|
|
1599
2120
|
const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
|
|
1600
2121
|
return tunneled(driver, tunnel, label);
|
|
1601
2122
|
});
|
|
@@ -1623,6 +2144,10 @@ function openDirect(config) {
|
|
|
1623
2144
|
return factories.oracle.open(config);
|
|
1624
2145
|
case "d1":
|
|
1625
2146
|
return factories.d1.open(config);
|
|
2147
|
+
case "do":
|
|
2148
|
+
return factories.do.open(config);
|
|
2149
|
+
case "do-remote":
|
|
2150
|
+
return factories["do-remote"].open(config);
|
|
1626
2151
|
}
|
|
1627
2152
|
}
|
|
1628
2153
|
function testDirect(config) {
|
|
@@ -1639,6 +2164,10 @@ function testDirect(config) {
|
|
|
1639
2164
|
return factories.oracle.test(config);
|
|
1640
2165
|
case "d1":
|
|
1641
2166
|
return factories.d1.test(config);
|
|
2167
|
+
case "do":
|
|
2168
|
+
return factories.do.test(config);
|
|
2169
|
+
case "do-remote":
|
|
2170
|
+
return factories["do-remote"].test(config);
|
|
1642
2171
|
}
|
|
1643
2172
|
}
|
|
1644
2173
|
|
|
@@ -1886,7 +2415,7 @@ async function updateNotice(current) {
|
|
|
1886
2415
|
|
|
1887
2416
|
// src/server.ts
|
|
1888
2417
|
import express from "express";
|
|
1889
|
-
import { createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2418
|
+
import { createHash as createHash2, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1890
2419
|
|
|
1891
2420
|
// src/tunneling.ts
|
|
1892
2421
|
import { randomUUID } from "crypto";
|
|
@@ -2116,6 +2645,19 @@ var ConnectionManager = class {
|
|
|
2116
2645
|
throw new ConnectionLostError(`Connection lost: ${err instanceof Error ? err.message : String(err)}`);
|
|
2117
2646
|
}
|
|
2118
2647
|
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Interrupt whatever is running on a connection. Best effort: an engine with
|
|
2650
|
+
* no way to interrupt (better-sqlite3 blocks the thread) reports false rather
|
|
2651
|
+
* than claiming a cancel that did not happen. The connection stays open —
|
|
2652
|
+
* the point is to free it, not to drop it.
|
|
2653
|
+
*/
|
|
2654
|
+
async cancel(connectionId) {
|
|
2655
|
+
const driver = this.connections.get(connectionId);
|
|
2656
|
+
if (!driver) throw new Error(`Connection not found: ${connectionId}`);
|
|
2657
|
+
if (!driver.cancel) return false;
|
|
2658
|
+
await driver.cancel();
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2119
2661
|
/**
|
|
2120
2662
|
* Close and remove a connection.
|
|
2121
2663
|
*/
|
|
@@ -2253,7 +2795,9 @@ function createAgent(options) {
|
|
|
2253
2795
|
const allowedOrigins = new Set(options.allowedOrigins.map((o) => o.replace(/\/+$/, "")));
|
|
2254
2796
|
const allowedHosts = options.allowedHosts ? new Set(options.allowedHosts.map((h) => h.toLowerCase())) : null;
|
|
2255
2797
|
const throttle = new AuthThrottle();
|
|
2798
|
+
const pairThrottle = new AuthThrottle();
|
|
2256
2799
|
let pairing = options.pairingCode;
|
|
2800
|
+
let lastPairOffer = 0;
|
|
2257
2801
|
app.set("env", "production");
|
|
2258
2802
|
app.disable("x-powered-by");
|
|
2259
2803
|
app.disable("etag");
|
|
@@ -2288,6 +2832,19 @@ function createAgent(options) {
|
|
|
2288
2832
|
});
|
|
2289
2833
|
app.use(accessControl(allowedOrigins));
|
|
2290
2834
|
if (options.appDir) serveApp(app, options.appDir);
|
|
2835
|
+
const PAIR_OFFER_INTERVAL_MS = 6e4;
|
|
2836
|
+
function offerPairing(req) {
|
|
2837
|
+
if (!options.onUnpaired) return false;
|
|
2838
|
+
const origin = req.headers.origin;
|
|
2839
|
+
if (typeof origin !== "string" || !allowedOrigins.has(origin.replace(/\/+$/, ""))) return false;
|
|
2840
|
+
const now = Date.now();
|
|
2841
|
+
if (pairing && now <= pairing.expiresAt) return false;
|
|
2842
|
+
if (now - lastPairOffer < PAIR_OFFER_INTERVAL_MS) return false;
|
|
2843
|
+
lastPairOffer = now;
|
|
2844
|
+
pairing = { code: randomUUID2(), expiresAt: now + 10 * 60 * 1e3 };
|
|
2845
|
+
options.onUnpaired(pairing, origin);
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2291
2848
|
app.get("/health", (req, res) => {
|
|
2292
2849
|
const body = {
|
|
2293
2850
|
success: true,
|
|
@@ -2300,23 +2857,21 @@ function createAgent(options) {
|
|
|
2300
2857
|
if (options.install) body.install = options.install;
|
|
2301
2858
|
if (options.gateway) body.gateway = { name: options.gateway.name, profiles: options.gateway.profiles.length };
|
|
2302
2859
|
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
2860
|
body.authorized = authorized(req);
|
|
2308
2861
|
}
|
|
2862
|
+
if (body.authorized !== true) void offerPairing(req);
|
|
2309
2863
|
res.json(body);
|
|
2310
2864
|
});
|
|
2311
2865
|
app.post("/pair", express.json({ limit: "1kb" }), (req, res) => {
|
|
2312
|
-
if (
|
|
2313
|
-
fail(res, 429, "Too many failed
|
|
2866
|
+
if (pairThrottle.locked(peerOf(req))) {
|
|
2867
|
+
fail(res, 429, "Too many failed pairing attempts; try again shortly");
|
|
2314
2868
|
return;
|
|
2315
2869
|
}
|
|
2316
2870
|
const code = typeof req.body?.code === "string" ? req.body.code : "";
|
|
2317
2871
|
if (!pairing || Date.now() > pairing.expiresAt || !code || !secretEquals(code, pairing.code)) {
|
|
2318
|
-
|
|
2319
|
-
|
|
2872
|
+
pairThrottle.fail(peerOf(req));
|
|
2873
|
+
const printed = offerPairing(req);
|
|
2874
|
+
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
2875
|
return;
|
|
2321
2876
|
}
|
|
2322
2877
|
pairing = void 0;
|
|
@@ -2406,6 +2961,24 @@ function createAgent(options) {
|
|
|
2406
2961
|
await flushUsage();
|
|
2407
2962
|
res.json(await usageSnapshot());
|
|
2408
2963
|
});
|
|
2964
|
+
app.post("/cancel", async (req, res) => {
|
|
2965
|
+
try {
|
|
2966
|
+
const connectionId = req.body?.connectionId;
|
|
2967
|
+
if (typeof connectionId !== "string" || !connectionId) {
|
|
2968
|
+
fail(res, 400, "connectionId is required");
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
const cancelled = await manager.cancel(connectionId);
|
|
2972
|
+
const body = {
|
|
2973
|
+
success: true,
|
|
2974
|
+
cancelled,
|
|
2975
|
+
message: cancelled ? "Cancel sent" : "This engine cannot interrupt a running statement"
|
|
2976
|
+
};
|
|
2977
|
+
res.json(body);
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
fail(res, errorStatus(err), errorMessage(err));
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2409
2982
|
app.post("/disconnect", async (req, res) => {
|
|
2410
2983
|
try {
|
|
2411
2984
|
const { connectionId } = req.body ?? {};
|
|
@@ -2633,7 +3206,7 @@ async function main() {
|
|
|
2633
3206
|
const port = Number(flags.port ?? process.env.PLANCHE_PORT) || 8888;
|
|
2634
3207
|
const addr = flags.addr ?? process.env.PLANCHE_ADDR ?? "127.0.0.1";
|
|
2635
3208
|
const tokenFromEnv = !flags.token && !!process.env.PLANCHE_AUTH_TOKEN;
|
|
2636
|
-
const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ??
|
|
3209
|
+
const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID3();
|
|
2637
3210
|
const gatewayFile = flags.gateway ?? process.env.PLANCHE_GATEWAY;
|
|
2638
3211
|
let gateway;
|
|
2639
3212
|
if (gatewayFile) {
|
|
@@ -2684,9 +3257,11 @@ async function main() {
|
|
|
2684
3257
|
const devApp = LOOPBACK_ADDRS.has(new URL(appUrl).hostname);
|
|
2685
3258
|
const allowedOrigins = [appOrigin, ...devApp ? DEV_ORIGINS : [], ...extraOrigins];
|
|
2686
3259
|
if (serveApp2) allowedOrigins.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`);
|
|
2687
|
-
const pairingCode = { code:
|
|
3260
|
+
const pairingCode = { code: randomUUID3(), expiresAt: Date.now() + 10 * 60 * 1e3 };
|
|
2688
3261
|
const listenAddr = addr.replace(/^\[(.*)\]$/, "$1");
|
|
2689
3262
|
const agentUrl = `http://${listenAddr.includes(":") ? `[${listenAddr}]` : listenAddr}:${port}`;
|
|
3263
|
+
const divider = "-".repeat(72);
|
|
3264
|
+
const pairLink = (code, appBase) => serveApp2 ? `${appBase}/#pair=${encodeURIComponent(code)}` : `${appBase}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(code)}`;
|
|
2690
3265
|
const { app, manager } = createAgent({
|
|
2691
3266
|
authToken,
|
|
2692
3267
|
allowedOrigins,
|
|
@@ -2698,17 +3273,30 @@ async function main() {
|
|
|
2698
3273
|
allowedHosts: loopback ? ["127.0.0.1", "localhost", "[::1]", `127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`] : void 0,
|
|
2699
3274
|
pairingCode,
|
|
2700
3275
|
gateway,
|
|
2701
|
-
appDir
|
|
3276
|
+
appDir,
|
|
3277
|
+
// An app that cannot authenticate gets a fresh link on this terminal
|
|
3278
|
+
// rather than needing the agent restarted. The link is built from the
|
|
3279
|
+
// origin the request came from, so it points at the app that is really
|
|
3280
|
+
// running — a dev server bumped to another port included.
|
|
3281
|
+
onUnpaired: (fresh, appOrigin2) => {
|
|
3282
|
+
const url = pairLink(fresh.code, serveApp2 ? publicUrl || appOrigin2 : appOrigin2);
|
|
3283
|
+
console.log(divider);
|
|
3284
|
+
console.log(`The app at ${appOrigin2} is not paired with this agent.`);
|
|
3285
|
+
console.log(`Pair it with (valid for 10 minutes, single use):
|
|
3286
|
+
|
|
3287
|
+
${url}
|
|
3288
|
+
`);
|
|
3289
|
+
console.log(divider);
|
|
3290
|
+
}
|
|
2702
3291
|
});
|
|
2703
|
-
const pairUrl = serveApp2 ?
|
|
3292
|
+
const pairUrl = pairLink(pairingCode.code, serveApp2 ? publicUrl || agentUrl : appUrl);
|
|
2704
3293
|
if (offlineFallback) {
|
|
2705
3294
|
console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
|
|
2706
3295
|
}
|
|
2707
3296
|
const server = app.listen(port, listenAddr, () => {
|
|
2708
|
-
|
|
2709
|
-
console.log(line);
|
|
3297
|
+
console.log(divider);
|
|
2710
3298
|
console.log(`Planche agent ${VERSION} listening on ${agentUrl}`);
|
|
2711
|
-
console.log(
|
|
3299
|
+
console.log(divider);
|
|
2712
3300
|
console.log(`Open the app with (link valid for 10 minutes, single use):
|
|
2713
3301
|
|
|
2714
3302
|
${pairUrl}
|
|
@@ -2719,7 +3307,7 @@ async function main() {
|
|
|
2719
3307
|
if (!loopback) {
|
|
2720
3308
|
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
3309
|
}
|
|
2722
|
-
console.log(
|
|
3310
|
+
console.log(divider);
|
|
2723
3311
|
if (shouldOpen) openInBrowser(pairUrl);
|
|
2724
3312
|
void updateNotice(VERSION).then((notice) => {
|
|
2725
3313
|
if (notice) console.log(notice);
|