@the-meridian/cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +8 -17
- package/dist/index.js +850 -620
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// src/commands/db.ts
|
|
9
|
-
import { readFileSync as
|
|
10
|
-
import { join as
|
|
9
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
10
|
+
import { join as join5 } from "path";
|
|
11
11
|
|
|
12
12
|
// src/version.ts
|
|
13
13
|
import { createRequire } from "module";
|
|
@@ -67,6 +67,7 @@ var deviceInfoResponseSchema = z.object({
|
|
|
67
67
|
created_at: z.string().optional()
|
|
68
68
|
}).loose();
|
|
69
69
|
var devDatabaseStatusSchema = z.union([z.enum(["provisioning", "ready", "error"]), z.string()]);
|
|
70
|
+
var devDatabaseAccessSchema = z.union([z.enum(["gateway", "proxy"]), z.string()]);
|
|
70
71
|
var devDatabaseSchema = z.object({
|
|
71
72
|
id: idSchema,
|
|
72
73
|
status: devDatabaseStatusSchema,
|
|
@@ -78,7 +79,21 @@ var devDatabaseSchema = z.object({
|
|
|
78
79
|
region: z.string().nullish(),
|
|
79
80
|
created_at: z.string().nullish(),
|
|
80
81
|
/** Set by the API only while `status` is `error`; surfaced verbatim to the developer. */
|
|
81
|
-
error_message: z.string().nullish()
|
|
82
|
+
error_message: z.string().nullish(),
|
|
83
|
+
/**
|
|
84
|
+
* How the CLI reaches the database. The API always reports `gateway` (the
|
|
85
|
+
* Meridian database gateway with a per-developer certificate) since
|
|
86
|
+
* MER-823; the CLI no longer reads it.
|
|
87
|
+
*/
|
|
88
|
+
access: devDatabaseAccessSchema.nullish()
|
|
89
|
+
}).loose();
|
|
90
|
+
var devDatabaseCertificateSchema = z.object({
|
|
91
|
+
certificate_pem: z.string(),
|
|
92
|
+
ca_pem: z.string(),
|
|
93
|
+
host: z.string(),
|
|
94
|
+
port: z.coerce.number(),
|
|
95
|
+
expires_at: z.string(),
|
|
96
|
+
fingerprint: z.string()
|
|
82
97
|
}).loose();
|
|
83
98
|
var devCredentialsSchema = z.object({
|
|
84
99
|
host: z.string(),
|
|
@@ -96,11 +111,6 @@ var devCredentialsSchema = z.object({
|
|
|
96
111
|
/** Echoed from the (nullable) dev-database column, so it can be null. */
|
|
97
112
|
instance_connection_name: z.string().nullish()
|
|
98
113
|
}).loose();
|
|
99
|
-
var proxyTokenSchema = z.object({
|
|
100
|
-
access_token: z.string(),
|
|
101
|
-
expires_at: z.string().nullish(),
|
|
102
|
-
instance_connection_name: z.string().nullish()
|
|
103
|
-
}).loose();
|
|
104
114
|
var migrationRunInputSchema = z.object({
|
|
105
115
|
command: z.string(),
|
|
106
116
|
status: z.enum(["success", "failed", "rolled_back"]),
|
|
@@ -335,11 +345,16 @@ var ApiClient = class _ApiClient {
|
|
|
335
345
|
`/app/cli/apps/${encodePathSegment(appId)}/dev-database/credentials`
|
|
336
346
|
);
|
|
337
347
|
}
|
|
338
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Signs `publicKeyPem` (SPKI) as this developer's client certificate for the
|
|
350
|
+
* database gateway. Each call replaces the previous certificate server-side.
|
|
351
|
+
*/
|
|
352
|
+
async issueDevDatabaseCertificate(appId, publicKeyPem) {
|
|
339
353
|
return this.request(
|
|
340
|
-
|
|
354
|
+
devDatabaseCertificateSchema,
|
|
341
355
|
"POST",
|
|
342
|
-
`/app/cli/apps/${encodePathSegment(appId)}/dev-database/
|
|
356
|
+
`/app/cli/apps/${encodePathSegment(appId)}/dev-database/certificate`,
|
|
357
|
+
{ body: { public_key: publicKeyPem } }
|
|
343
358
|
);
|
|
344
359
|
}
|
|
345
360
|
async resetDevDatabase(appId) {
|
|
@@ -523,7 +538,9 @@ function extractErrorCode(body) {
|
|
|
523
538
|
}
|
|
524
539
|
var HINTS_BY_ERROR_CODE = {
|
|
525
540
|
dev_database_limit_reached: "Your plan does not include a developer database for you on this app. Every collaborator gets their own out of the plan's allowance, so upgrading the organization's plan in the Meridian dashboard is what unlocks it.",
|
|
541
|
+
dev_databases_unavailable: "This Meridian environment does not offer developer databases. Run `meridian dev` against production (the default API URL) instead.",
|
|
526
542
|
dev_database_over_storage_limit: "Run `meridian db reset` to drop and recreate the schema. Reset and delete stay available while you're over the limit.",
|
|
543
|
+
cli_upgrade_required: "Upgrade the Meridian CLI: npm i -g @the-meridian/cli",
|
|
527
544
|
EXTERNAL_ACCESS_UNAVAILABLE: "External access is not switched on for this database yet, or the database is not ready. Check the database under Hosting in the Meridian dashboard.",
|
|
528
545
|
FEATURE_NOT_ENTITLED: "Your organization's plan does not include managed developer databases; upgrade it in the Meridian dashboard."
|
|
529
546
|
};
|
|
@@ -563,9 +580,6 @@ function meridianHome() {
|
|
|
563
580
|
function globalConfigPath() {
|
|
564
581
|
return join(meridianHome(), "config.json");
|
|
565
582
|
}
|
|
566
|
-
function proxyBinDir() {
|
|
567
|
-
return join(meridianHome(), "bin");
|
|
568
|
-
}
|
|
569
583
|
function projectStateDir(root) {
|
|
570
584
|
return join(root, ".meridian");
|
|
571
585
|
}
|
|
@@ -718,8 +732,8 @@ function connectionInfoFromCredentials(credentials, proxy) {
|
|
|
718
732
|
};
|
|
719
733
|
}
|
|
720
734
|
var createSqlConnection = async (info, options) => {
|
|
721
|
-
const { createConnection
|
|
722
|
-
const connection = await
|
|
735
|
+
const { createConnection } = await import("mysql2/promise");
|
|
736
|
+
const connection = await createConnection({
|
|
723
737
|
host: info.host,
|
|
724
738
|
port: info.port,
|
|
725
739
|
user: info.user,
|
|
@@ -803,7 +817,7 @@ function toDatabaseError(error2, info) {
|
|
|
803
817
|
if (code === "ECONNREFUSED") {
|
|
804
818
|
return build(
|
|
805
819
|
`Could not reach ${target} (${code}).`,
|
|
806
|
-
"Nothing is listening on that port. `meridian dev` and `meridian migrate` start the
|
|
820
|
+
"Nothing is listening on that port. `meridian dev` and `meridian migrate` start the database connection for you; start one of them first if you are connecting by hand."
|
|
807
821
|
);
|
|
808
822
|
}
|
|
809
823
|
if (code === "ETIMEDOUT" || code === "PROTOCOL_SEQUENCE_TIMEOUT") {
|
|
@@ -1742,14 +1756,770 @@ function redactDatabaseUrl(url) {
|
|
|
1742
1756
|
return url.replace(/^(mysql:\/\/[^:@/]+):([^@]*)@/, "$1:****@");
|
|
1743
1757
|
}
|
|
1744
1758
|
|
|
1759
|
+
// src/lib/gateway/certificate.ts
|
|
1760
|
+
import { generateKeyPair } from "crypto";
|
|
1761
|
+
import { createSecureContext } from "tls";
|
|
1762
|
+
import { promisify } from "util";
|
|
1763
|
+
var generateKeyPairAsync = promisify(generateKeyPair);
|
|
1764
|
+
function gatewayCertificateFromPem(material) {
|
|
1765
|
+
return {
|
|
1766
|
+
host: material.host,
|
|
1767
|
+
port: material.port,
|
|
1768
|
+
expiresAt: material.expiresAt,
|
|
1769
|
+
fingerprint: material.fingerprint,
|
|
1770
|
+
secureContext: createSecureContext({
|
|
1771
|
+
key: material.keyPem,
|
|
1772
|
+
cert: material.certificatePem,
|
|
1773
|
+
ca: material.caPem,
|
|
1774
|
+
minVersion: "TLSv1.2"
|
|
1775
|
+
})
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
async function generateGatewayKeyPair() {
|
|
1779
|
+
const { publicKey, privateKey } = await generateKeyPairAsync("ec", { namedCurve: "prime256v1" });
|
|
1780
|
+
return {
|
|
1781
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
1782
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString()
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
async function issueGatewayCertificate(client, appId) {
|
|
1786
|
+
const { publicKeyPem, privateKeyPem } = await generateGatewayKeyPair();
|
|
1787
|
+
const issued = await client.issueDevDatabaseCertificate(appId, publicKeyPem);
|
|
1788
|
+
return gatewayCertificateFromPem({
|
|
1789
|
+
keyPem: privateKeyPem,
|
|
1790
|
+
certificatePem: issued.certificate_pem,
|
|
1791
|
+
caPem: issued.ca_pem,
|
|
1792
|
+
host: issued.host,
|
|
1793
|
+
port: issued.port,
|
|
1794
|
+
expiresAt: issued.expires_at,
|
|
1795
|
+
fingerprint: issued.fingerprint
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
var ASSUMED_LIFETIME_MS = 24 * 60 * 6e4;
|
|
1799
|
+
var DEFAULT_REFRESH_BEFORE_MS = 4 * 60 * 6e4;
|
|
1800
|
+
var MIN_REFRESH_DELAY_MS = 6e4;
|
|
1801
|
+
function certificateRefreshDelay(expiresAt, now, refreshBeforeMs = DEFAULT_REFRESH_BEFORE_MS) {
|
|
1802
|
+
const parsed = Date.parse(expiresAt);
|
|
1803
|
+
const expiry = Number.isNaN(parsed) ? now + ASSUMED_LIFETIME_MS : parsed;
|
|
1804
|
+
return Math.max(MIN_REFRESH_DELAY_MS, expiry - refreshBeforeMs - now);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/lib/gateway/forwarder.ts
|
|
1808
|
+
import { connect as netConnect, createServer, isIP } from "net";
|
|
1809
|
+
import { connect as tlsConnect } from "tls";
|
|
1810
|
+
|
|
1811
|
+
// src/lib/gateway/packets.ts
|
|
1812
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
1813
|
+
var CLIENT = {
|
|
1814
|
+
LONG_PASSWORD: 1,
|
|
1815
|
+
FOUND_ROWS: 2,
|
|
1816
|
+
LONG_FLAG: 4,
|
|
1817
|
+
CONNECT_WITH_DB: 8,
|
|
1818
|
+
NO_SCHEMA: 16,
|
|
1819
|
+
COMPRESS: 32,
|
|
1820
|
+
ODBC: 64,
|
|
1821
|
+
LOCAL_FILES: 128,
|
|
1822
|
+
IGNORE_SPACE: 256,
|
|
1823
|
+
PROTOCOL_41: 512,
|
|
1824
|
+
INTERACTIVE: 1024,
|
|
1825
|
+
SSL: 2048,
|
|
1826
|
+
IGNORE_SIGPIPE: 4096,
|
|
1827
|
+
TRANSACTIONS: 8192,
|
|
1828
|
+
RESERVED: 16384,
|
|
1829
|
+
SECURE_CONNECTION: 32768,
|
|
1830
|
+
MULTI_STATEMENTS: 65536,
|
|
1831
|
+
MULTI_RESULTS: 131072,
|
|
1832
|
+
PS_MULTI_RESULTS: 262144,
|
|
1833
|
+
PLUGIN_AUTH: 524288,
|
|
1834
|
+
CONNECT_ATTRS: 1048576,
|
|
1835
|
+
PLUGIN_AUTH_LENENC_CLIENT_DATA: 2097152,
|
|
1836
|
+
CAN_HANDLE_EXPIRED_PASSWORDS: 4194304,
|
|
1837
|
+
SESSION_TRACK: 8388608,
|
|
1838
|
+
DEPRECATE_EOF: 16777216,
|
|
1839
|
+
OPTIONAL_RESULTSET_METADATA: 33554432,
|
|
1840
|
+
ZSTD_COMPRESSION: 67108864,
|
|
1841
|
+
QUERY_ATTRIBUTES: 134217728
|
|
1842
|
+
};
|
|
1843
|
+
var GATEWAY_CAPABILITIES = CLIENT.LONG_PASSWORD | CLIENT.FOUND_ROWS | CLIENT.LONG_FLAG | CLIENT.CONNECT_WITH_DB | CLIENT.IGNORE_SPACE | CLIENT.PROTOCOL_41 | CLIENT.INTERACTIVE | CLIENT.SSL | CLIENT.IGNORE_SIGPIPE | CLIENT.TRANSACTIONS | CLIENT.RESERVED | CLIENT.SECURE_CONNECTION | CLIENT.MULTI_STATEMENTS | CLIENT.MULTI_RESULTS | CLIENT.PS_MULTI_RESULTS | CLIENT.PLUGIN_AUTH | CLIENT.CONNECT_ATTRS | CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA | CLIENT.CAN_HANDLE_EXPIRED_PASSWORDS | CLIENT.SESSION_TRACK | CLIENT.DEPRECATE_EOF;
|
|
1844
|
+
var LOCAL_CAPABILITIES = GATEWAY_CAPABILITIES & ~CLIENT.SSL;
|
|
1845
|
+
var PLUGIN_NATIVE = "mysql_native_password";
|
|
1846
|
+
var PLUGIN_CACHING_SHA2 = "caching_sha2_password";
|
|
1847
|
+
var SCRAMBLE_LENGTH = 20;
|
|
1848
|
+
var DEFAULT_CHARSET = 255;
|
|
1849
|
+
var MAX_PAYLOAD = 16777215;
|
|
1850
|
+
var MAX_HANDSHAKE_PACKET = 64 * 1024;
|
|
1851
|
+
var AUTH_MORE_DATA = 1;
|
|
1852
|
+
var FAST_AUTH_SUCCESS = 3;
|
|
1853
|
+
var PERFORM_FULL_AUTH = 4;
|
|
1854
|
+
var SERVER_STATUS_AUTOCOMMIT = 2;
|
|
1855
|
+
function encodePacket(seq, payload) {
|
|
1856
|
+
if (payload.length >= MAX_PAYLOAD) throw new Error(`refusing to write a ${payload.length} byte handshake packet`);
|
|
1857
|
+
const header = Buffer.alloc(4);
|
|
1858
|
+
header.writeUIntLE(payload.length, 0, 3);
|
|
1859
|
+
header[3] = seq & 255;
|
|
1860
|
+
return Buffer.concat([header, payload]);
|
|
1861
|
+
}
|
|
1862
|
+
var PacketReader = class {
|
|
1863
|
+
#socket;
|
|
1864
|
+
#maxPacket;
|
|
1865
|
+
#buffer = Buffer.alloc(0);
|
|
1866
|
+
#failure;
|
|
1867
|
+
#waiter;
|
|
1868
|
+
#onData = (chunk) => {
|
|
1869
|
+
this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
|
|
1870
|
+
this.#settle();
|
|
1871
|
+
};
|
|
1872
|
+
#onClose = () => {
|
|
1873
|
+
this.#fail(new Error("the connection closed during the MySQL handshake"));
|
|
1874
|
+
};
|
|
1875
|
+
#onError = (error2) => {
|
|
1876
|
+
this.#fail(error2);
|
|
1877
|
+
};
|
|
1878
|
+
constructor(socket, maxPacket = MAX_HANDSHAKE_PACKET) {
|
|
1879
|
+
this.#socket = socket;
|
|
1880
|
+
this.#maxPacket = maxPacket;
|
|
1881
|
+
socket.on("data", this.#onData);
|
|
1882
|
+
socket.on("end", this.#onClose);
|
|
1883
|
+
socket.on("close", this.#onClose);
|
|
1884
|
+
socket.on("error", this.#onError);
|
|
1885
|
+
}
|
|
1886
|
+
read() {
|
|
1887
|
+
return new Promise((resolve2, reject) => {
|
|
1888
|
+
this.#waiter = { resolve: resolve2, reject };
|
|
1889
|
+
this.#settle();
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
/** Stops reading. `pause` keeps later bytes in the socket for a pipe() to pick up. */
|
|
1893
|
+
release(options) {
|
|
1894
|
+
if (options.pause) this.#socket.pause();
|
|
1895
|
+
this.#socket.off("data", this.#onData);
|
|
1896
|
+
this.#socket.off("end", this.#onClose);
|
|
1897
|
+
this.#socket.off("close", this.#onClose);
|
|
1898
|
+
this.#socket.off("error", this.#onError);
|
|
1899
|
+
const rest = this.#buffer;
|
|
1900
|
+
this.#buffer = Buffer.alloc(0);
|
|
1901
|
+
return rest;
|
|
1902
|
+
}
|
|
1903
|
+
#fail(error2) {
|
|
1904
|
+
if (this.#failure === void 0) this.#failure = error2;
|
|
1905
|
+
this.#settle();
|
|
1906
|
+
}
|
|
1907
|
+
#settle() {
|
|
1908
|
+
const waiter = this.#waiter;
|
|
1909
|
+
if (waiter === void 0) return;
|
|
1910
|
+
if (this.#buffer.length >= 4) {
|
|
1911
|
+
const length = this.#buffer.readUIntLE(0, 3);
|
|
1912
|
+
if (length > this.#maxPacket) {
|
|
1913
|
+
this.#waiter = void 0;
|
|
1914
|
+
waiter.reject(new Error(`a ${length} byte packet during the MySQL handshake is too large`));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (this.#buffer.length >= 4 + length) {
|
|
1918
|
+
const seq = this.#buffer[3] ?? 0;
|
|
1919
|
+
const payload = Buffer.from(this.#buffer.subarray(4, 4 + length));
|
|
1920
|
+
this.#buffer = this.#buffer.subarray(4 + length);
|
|
1921
|
+
this.#waiter = void 0;
|
|
1922
|
+
waiter.resolve({ seq, payload });
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (this.#failure !== void 0) {
|
|
1927
|
+
this.#waiter = void 0;
|
|
1928
|
+
waiter.reject(this.#failure);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
var Cursor = class {
|
|
1933
|
+
constructor(buf) {
|
|
1934
|
+
this.buf = buf;
|
|
1935
|
+
}
|
|
1936
|
+
buf;
|
|
1937
|
+
pos = 0;
|
|
1938
|
+
get remaining() {
|
|
1939
|
+
return this.buf.length - this.pos;
|
|
1940
|
+
}
|
|
1941
|
+
bytes(n, what) {
|
|
1942
|
+
if (n < 0 || this.remaining < n) throw new Error(`truncated packet reading ${what}`);
|
|
1943
|
+
const out = this.buf.subarray(this.pos, this.pos + n);
|
|
1944
|
+
this.pos += n;
|
|
1945
|
+
return out;
|
|
1946
|
+
}
|
|
1947
|
+
u8(what) {
|
|
1948
|
+
return this.bytes(1, what)[0] ?? 0;
|
|
1949
|
+
}
|
|
1950
|
+
u16(what) {
|
|
1951
|
+
return this.bytes(2, what).readUInt16LE(0);
|
|
1952
|
+
}
|
|
1953
|
+
u32(what) {
|
|
1954
|
+
return this.bytes(4, what).readUInt32LE(0);
|
|
1955
|
+
}
|
|
1956
|
+
nulString(what) {
|
|
1957
|
+
const end = this.buf.indexOf(0, this.pos);
|
|
1958
|
+
if (end < 0) throw new Error(`truncated packet reading ${what}`);
|
|
1959
|
+
const out = this.buf.subarray(this.pos, end);
|
|
1960
|
+
this.pos = end + 1;
|
|
1961
|
+
return out;
|
|
1962
|
+
}
|
|
1963
|
+
/** Everything left, without a trailing NUL. */
|
|
1964
|
+
restNul() {
|
|
1965
|
+
let out = this.buf.subarray(this.pos);
|
|
1966
|
+
this.pos = this.buf.length;
|
|
1967
|
+
if (out.length > 0 && out[out.length - 1] === 0) out = out.subarray(0, out.length - 1);
|
|
1968
|
+
return out;
|
|
1969
|
+
}
|
|
1970
|
+
lenencInt(what) {
|
|
1971
|
+
const first = this.u8(what);
|
|
1972
|
+
if (first < 251) return first;
|
|
1973
|
+
if (first === 252) return this.u16(what);
|
|
1974
|
+
if (first === 253) return this.bytes(3, what).readUIntLE(0, 3);
|
|
1975
|
+
if (first === 254) {
|
|
1976
|
+
const value = this.bytes(8, what).readBigUInt64LE(0);
|
|
1977
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${what} is too large`);
|
|
1978
|
+
return Number(value);
|
|
1979
|
+
}
|
|
1980
|
+
throw new Error(`invalid length-encoded integer reading ${what}`);
|
|
1981
|
+
}
|
|
1982
|
+
lenencBytes(what) {
|
|
1983
|
+
return this.bytes(this.lenencInt(what), what);
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
function lenencInt(n) {
|
|
1987
|
+
if (n < 251) return Buffer.from([n]);
|
|
1988
|
+
if (n <= 65535) {
|
|
1989
|
+
const b2 = Buffer.alloc(3);
|
|
1990
|
+
b2[0] = 252;
|
|
1991
|
+
b2.writeUInt16LE(n, 1);
|
|
1992
|
+
return b2;
|
|
1993
|
+
}
|
|
1994
|
+
if (n <= 16777215) {
|
|
1995
|
+
const b2 = Buffer.alloc(4);
|
|
1996
|
+
b2[0] = 253;
|
|
1997
|
+
b2.writeUIntLE(n, 1, 3);
|
|
1998
|
+
return b2;
|
|
1999
|
+
}
|
|
2000
|
+
const b = Buffer.alloc(9);
|
|
2001
|
+
b[0] = 254;
|
|
2002
|
+
b.writeBigUInt64LE(BigInt(n), 1);
|
|
2003
|
+
return b;
|
|
2004
|
+
}
|
|
2005
|
+
function lenencBytes(value) {
|
|
2006
|
+
return Buffer.concat([lenencInt(value.length), value]);
|
|
2007
|
+
}
|
|
2008
|
+
function u16(n) {
|
|
2009
|
+
const b = Buffer.alloc(2);
|
|
2010
|
+
b.writeUInt16LE(n & 65535, 0);
|
|
2011
|
+
return b;
|
|
2012
|
+
}
|
|
2013
|
+
function u32(n) {
|
|
2014
|
+
const b = Buffer.alloc(4);
|
|
2015
|
+
b.writeUInt32LE(n >>> 0, 0);
|
|
2016
|
+
return b;
|
|
2017
|
+
}
|
|
2018
|
+
var NUL = Buffer.from([0]);
|
|
2019
|
+
function encodeGreeting(g) {
|
|
2020
|
+
if (g.scramble.length !== SCRAMBLE_LENGTH) throw new Error("the greeting scramble must be 20 bytes");
|
|
2021
|
+
return Buffer.concat([
|
|
2022
|
+
Buffer.from([10]),
|
|
2023
|
+
Buffer.from(g.serverVersion, "utf8"),
|
|
2024
|
+
NUL,
|
|
2025
|
+
u32(g.connectionId),
|
|
2026
|
+
g.scramble.subarray(0, 8),
|
|
2027
|
+
NUL,
|
|
2028
|
+
u16(g.capabilities),
|
|
2029
|
+
Buffer.from([g.charset]),
|
|
2030
|
+
u16(g.status),
|
|
2031
|
+
u16(g.capabilities >>> 16),
|
|
2032
|
+
Buffer.from([SCRAMBLE_LENGTH + 1]),
|
|
2033
|
+
Buffer.alloc(10),
|
|
2034
|
+
g.scramble.subarray(8),
|
|
2035
|
+
NUL,
|
|
2036
|
+
Buffer.from(g.authPlugin, "utf8"),
|
|
2037
|
+
NUL
|
|
2038
|
+
]);
|
|
2039
|
+
}
|
|
2040
|
+
function localGreeting(connectionId, scramble, serverVersion) {
|
|
2041
|
+
return {
|
|
2042
|
+
serverVersion,
|
|
2043
|
+
connectionId,
|
|
2044
|
+
scramble,
|
|
2045
|
+
capabilities: LOCAL_CAPABILITIES,
|
|
2046
|
+
charset: DEFAULT_CHARSET,
|
|
2047
|
+
status: SERVER_STATUS_AUTOCOMMIT,
|
|
2048
|
+
authPlugin: PLUGIN_NATIVE
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
function parseGreeting(payload) {
|
|
2052
|
+
if (payload[0] === 255) throw serverErrorFromPacket(payload);
|
|
2053
|
+
const r = new Cursor(payload);
|
|
2054
|
+
const version = r.u8("protocol version");
|
|
2055
|
+
if (version !== 10) throw new Error(`unsupported handshake version ${version}`);
|
|
2056
|
+
const serverVersion = r.nulString("server version").toString("utf8");
|
|
2057
|
+
const connectionId = r.u32("connection id");
|
|
2058
|
+
const part1 = Buffer.from(r.bytes(8, "scramble part 1"));
|
|
2059
|
+
r.u8("filler");
|
|
2060
|
+
let capabilities = r.u16("capabilities");
|
|
2061
|
+
let charset = DEFAULT_CHARSET;
|
|
2062
|
+
let status = 0;
|
|
2063
|
+
let authPlugin = "";
|
|
2064
|
+
let scramble = part1;
|
|
2065
|
+
if (r.remaining > 0) {
|
|
2066
|
+
charset = r.u8("charset");
|
|
2067
|
+
status = r.u16("status");
|
|
2068
|
+
capabilities = (capabilities | r.u16("capabilities upper") << 16) >>> 0;
|
|
2069
|
+
const authLength = r.u8("auth data length");
|
|
2070
|
+
r.bytes(10, "reserved");
|
|
2071
|
+
if ((capabilities & CLIENT.SECURE_CONNECTION) !== 0) {
|
|
2072
|
+
let part2 = r.bytes(Math.max(13, authLength - 8), "scramble part 2");
|
|
2073
|
+
if (part2.length > 0 && part2[part2.length - 1] === 0) part2 = part2.subarray(0, part2.length - 1);
|
|
2074
|
+
scramble = Buffer.concat([part1, part2]);
|
|
2075
|
+
}
|
|
2076
|
+
if ((capabilities & CLIENT.PLUGIN_AUTH) !== 0) authPlugin = r.restNul().toString("utf8");
|
|
2077
|
+
}
|
|
2078
|
+
if (scramble.length < SCRAMBLE_LENGTH) throw new Error(`the server scramble is ${scramble.length} bytes`);
|
|
2079
|
+
return {
|
|
2080
|
+
serverVersion,
|
|
2081
|
+
connectionId,
|
|
2082
|
+
scramble: scramble.subarray(0, SCRAMBLE_LENGTH),
|
|
2083
|
+
capabilities,
|
|
2084
|
+
charset,
|
|
2085
|
+
status,
|
|
2086
|
+
authPlugin: authPlugin === "" ? PLUGIN_NATIVE : authPlugin
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
function encodeSslRequest(capabilities, maxPacketSize, charset) {
|
|
2090
|
+
return Buffer.concat([u32(capabilities | CLIENT.SSL), u32(maxPacketSize), Buffer.from([charset]), Buffer.alloc(23)]);
|
|
2091
|
+
}
|
|
2092
|
+
function parseHandshakeResponse(payload) {
|
|
2093
|
+
const r = new Cursor(payload);
|
|
2094
|
+
const capabilities = r.u32("capabilities");
|
|
2095
|
+
if ((capabilities & CLIENT.PROTOCOL_41) === 0) throw new Error("the client does not speak protocol 4.1");
|
|
2096
|
+
const maxPacketSize = r.u32("max packet size");
|
|
2097
|
+
const charset = r.u8("charset");
|
|
2098
|
+
r.bytes(23, "filler");
|
|
2099
|
+
if (r.remaining === 0) {
|
|
2100
|
+
return { capabilities, maxPacketSize, charset, username: "", authResponse: Buffer.alloc(0), database: "", authPlugin: "", attributes: [] };
|
|
2101
|
+
}
|
|
2102
|
+
const username = r.nulString("username").toString("utf8");
|
|
2103
|
+
let authResponse;
|
|
2104
|
+
if ((capabilities & CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA) !== 0) authResponse = r.lenencBytes("auth response");
|
|
2105
|
+
else if ((capabilities & CLIENT.SECURE_CONNECTION) !== 0) authResponse = r.bytes(r.u8("auth response length"), "auth response");
|
|
2106
|
+
else authResponse = r.nulString("auth response");
|
|
2107
|
+
let database = "";
|
|
2108
|
+
if ((capabilities & CLIENT.CONNECT_WITH_DB) !== 0 && r.remaining > 0) database = r.nulString("database").toString("utf8");
|
|
2109
|
+
let authPlugin = "";
|
|
2110
|
+
if ((capabilities & CLIENT.PLUGIN_AUTH) !== 0 && r.remaining > 0) authPlugin = r.nulString("auth plugin").toString("utf8");
|
|
2111
|
+
const attributes = [];
|
|
2112
|
+
if ((capabilities & CLIENT.CONNECT_ATTRS) !== 0 && r.remaining > 0) {
|
|
2113
|
+
const attrs = new Cursor(r.lenencBytes("attributes"));
|
|
2114
|
+
while (attrs.remaining > 0) {
|
|
2115
|
+
const key = attrs.lenencBytes("attribute key").toString("utf8");
|
|
2116
|
+
const value = attrs.lenencBytes("attribute value").toString("utf8");
|
|
2117
|
+
attributes.push([key, value]);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return {
|
|
2121
|
+
capabilities,
|
|
2122
|
+
maxPacketSize,
|
|
2123
|
+
charset,
|
|
2124
|
+
username,
|
|
2125
|
+
authResponse: Buffer.from(authResponse),
|
|
2126
|
+
database,
|
|
2127
|
+
authPlugin,
|
|
2128
|
+
attributes
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function encodeHandshakeResponse(h) {
|
|
2132
|
+
const parts = [u32(h.capabilities), u32(h.maxPacketSize), Buffer.from([h.charset]), Buffer.alloc(23)];
|
|
2133
|
+
parts.push(Buffer.from(h.username, "utf8"), NUL);
|
|
2134
|
+
if ((h.capabilities & CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA) !== 0) parts.push(lenencBytes(h.authResponse));
|
|
2135
|
+
else if ((h.capabilities & CLIENT.SECURE_CONNECTION) !== 0) parts.push(Buffer.from([h.authResponse.length]), h.authResponse);
|
|
2136
|
+
else parts.push(h.authResponse, NUL);
|
|
2137
|
+
if ((h.capabilities & CLIENT.CONNECT_WITH_DB) !== 0) parts.push(Buffer.from(h.database, "utf8"), NUL);
|
|
2138
|
+
if ((h.capabilities & CLIENT.PLUGIN_AUTH) !== 0) parts.push(Buffer.from(h.authPlugin, "utf8"), NUL);
|
|
2139
|
+
if ((h.capabilities & CLIENT.CONNECT_ATTRS) !== 0) {
|
|
2140
|
+
const attrs = h.attributes.map(
|
|
2141
|
+
([key, value]) => Buffer.concat([lenencBytes(Buffer.from(key, "utf8")), lenencBytes(Buffer.from(value, "utf8"))])
|
|
2142
|
+
);
|
|
2143
|
+
parts.push(lenencBytes(Buffer.concat(attrs)));
|
|
2144
|
+
}
|
|
2145
|
+
return Buffer.concat(parts);
|
|
2146
|
+
}
|
|
2147
|
+
function encodeAuthSwitchRequest(plugin, scramble) {
|
|
2148
|
+
return Buffer.concat([Buffer.from([254]), Buffer.from(plugin, "utf8"), NUL, scramble, NUL]);
|
|
2149
|
+
}
|
|
2150
|
+
function parseAuthSwitchRequest(payload) {
|
|
2151
|
+
if (payload[0] !== 254) throw new Error("not an auth switch request");
|
|
2152
|
+
const r = new Cursor(payload);
|
|
2153
|
+
r.pos = 1;
|
|
2154
|
+
const plugin = r.nulString("auth plugin").toString("utf8");
|
|
2155
|
+
let scramble = Buffer.from(r.restNul());
|
|
2156
|
+
if (scramble.length > SCRAMBLE_LENGTH) scramble = scramble.subarray(0, SCRAMBLE_LENGTH);
|
|
2157
|
+
return { plugin, scramble };
|
|
2158
|
+
}
|
|
2159
|
+
var isOk = (payload) => payload[0] === 0;
|
|
2160
|
+
var isErr = (payload) => payload[0] === 255;
|
|
2161
|
+
var isAuthSwitch = (payload) => payload[0] === 254;
|
|
2162
|
+
var isAuthMoreData = (payload) => payload[0] === AUTH_MORE_DATA;
|
|
2163
|
+
function encodeErr(code, sqlState, message) {
|
|
2164
|
+
const state2 = sqlState.length === 5 ? sqlState : "HY000";
|
|
2165
|
+
return Buffer.concat([Buffer.from([255]), u16(code), Buffer.from(`#${state2}`, "latin1"), Buffer.from(message, "utf8")]);
|
|
2166
|
+
}
|
|
2167
|
+
var MysqlServerError = class extends Error {
|
|
2168
|
+
constructor(code, sqlState, message) {
|
|
2169
|
+
super(message);
|
|
2170
|
+
this.code = code;
|
|
2171
|
+
this.sqlState = sqlState;
|
|
2172
|
+
this.name = "MysqlServerError";
|
|
2173
|
+
}
|
|
2174
|
+
code;
|
|
2175
|
+
sqlState;
|
|
2176
|
+
};
|
|
2177
|
+
function serverErrorFromPacket(payload) {
|
|
2178
|
+
if (payload.length < 3 || payload[0] !== 255) return new MysqlServerError(0, "HY000", "unknown error");
|
|
2179
|
+
const code = payload.readUInt16LE(1);
|
|
2180
|
+
let rest = payload.subarray(3);
|
|
2181
|
+
let sqlState = "HY000";
|
|
2182
|
+
if (rest.length >= 6 && rest[0] === 35) {
|
|
2183
|
+
sqlState = rest.subarray(1, 6).toString("latin1");
|
|
2184
|
+
rest = rest.subarray(6);
|
|
2185
|
+
}
|
|
2186
|
+
return new MysqlServerError(code, sqlState, rest.toString("utf8"));
|
|
2187
|
+
}
|
|
2188
|
+
function newScramble() {
|
|
2189
|
+
const b = randomBytes(SCRAMBLE_LENGTH);
|
|
2190
|
+
for (let i = 0; i < b.length; i++) {
|
|
2191
|
+
let v = (b[i] ?? 0) & 127 | 1;
|
|
2192
|
+
if (v === 36) v = 37;
|
|
2193
|
+
b[i] = v;
|
|
2194
|
+
}
|
|
2195
|
+
return b;
|
|
2196
|
+
}
|
|
2197
|
+
function xor(a, b) {
|
|
2198
|
+
const out = Buffer.alloc(a.length);
|
|
2199
|
+
for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
2200
|
+
return out;
|
|
2201
|
+
}
|
|
2202
|
+
function scrambleNative(password, nonce) {
|
|
2203
|
+
if (password === "") return Buffer.alloc(0);
|
|
2204
|
+
const h1 = createHash("sha1").update(password, "utf8").digest();
|
|
2205
|
+
const h2 = createHash("sha1").update(h1).digest();
|
|
2206
|
+
const h3 = createHash("sha1").update(nonce).update(h2).digest();
|
|
2207
|
+
return xor(h1, h3);
|
|
2208
|
+
}
|
|
2209
|
+
function scrambleCachingSha2(password, nonce) {
|
|
2210
|
+
if (password === "") return Buffer.alloc(0);
|
|
2211
|
+
const h1 = createHash("sha256").update(password, "utf8").digest();
|
|
2212
|
+
const h2 = createHash("sha256").update(h1).digest();
|
|
2213
|
+
const h3 = createHash("sha256").update(h2).update(nonce).digest();
|
|
2214
|
+
return xor(h1, h3);
|
|
2215
|
+
}
|
|
2216
|
+
function scrambleFor(plugin, password, nonce) {
|
|
2217
|
+
switch (plugin) {
|
|
2218
|
+
case PLUGIN_NATIVE:
|
|
2219
|
+
return scrambleNative(password, nonce);
|
|
2220
|
+
case PLUGIN_CACHING_SHA2:
|
|
2221
|
+
return scrambleCachingSha2(password, nonce);
|
|
2222
|
+
default:
|
|
2223
|
+
throw new Error(`unsupported auth plugin "${plugin}"`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function scrambleMatches(received, expected) {
|
|
2227
|
+
return received.length === expected.length && timingSafeEqual(received, expected);
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/lib/gateway/forwarder.ts
|
|
2231
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
2232
|
+
var DEFAULT_HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
2233
|
+
var REFRESH_RETRY_BACKOFFS_MS = [3e4, 6e4, 12e4, 3e5];
|
|
2234
|
+
var SERVER_VERSION = "8.0.40-meridian-forwarder";
|
|
2235
|
+
var CLOSE_GRACE_MS = 2e3;
|
|
2236
|
+
var ER_ACCESS_DENIED = 1045;
|
|
2237
|
+
var ER_HANDSHAKE = 1043;
|
|
2238
|
+
var CR_CONN_HOST_ERROR = 2003;
|
|
2239
|
+
var LocalRefusal = class extends Error {
|
|
2240
|
+
constructor(code, sqlState, message) {
|
|
2241
|
+
super(message);
|
|
2242
|
+
this.code = code;
|
|
2243
|
+
this.sqlState = sqlState;
|
|
2244
|
+
}
|
|
2245
|
+
code;
|
|
2246
|
+
sqlState;
|
|
2247
|
+
};
|
|
2248
|
+
function accessDenied(username) {
|
|
2249
|
+
return new LocalRefusal(ER_ACCESS_DENIED, "28000", `Access denied for user '${username}'@'localhost' (using password: YES)`);
|
|
2250
|
+
}
|
|
2251
|
+
function upstreamCapabilities(clientCapabilities, database) {
|
|
2252
|
+
let caps = clientCapabilities & LOCAL_CAPABILITIES | CLIENT.SSL | CLIENT.PROTOCOL_41 | CLIENT.SECURE_CONNECTION | CLIENT.PLUGIN_AUTH | CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA;
|
|
2253
|
+
if (database !== "") caps |= CLIENT.CONNECT_WITH_DB;
|
|
2254
|
+
return caps >>> 0;
|
|
2255
|
+
}
|
|
2256
|
+
function waitFor(socket, event) {
|
|
2257
|
+
return new Promise((resolve2, reject) => {
|
|
2258
|
+
const onError = (error2) => {
|
|
2259
|
+
socket.off(event, onDone);
|
|
2260
|
+
reject(error2);
|
|
2261
|
+
};
|
|
2262
|
+
const onDone = () => {
|
|
2263
|
+
socket.off("error", onError);
|
|
2264
|
+
resolve2();
|
|
2265
|
+
};
|
|
2266
|
+
socket.once(event, onDone);
|
|
2267
|
+
socket.once("error", onError);
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
function splice(a, b) {
|
|
2271
|
+
const closeOther = (other) => () => {
|
|
2272
|
+
if (other.destroyed) return;
|
|
2273
|
+
other.end();
|
|
2274
|
+
setTimeout(() => other.destroy(), CLOSE_GRACE_MS).unref();
|
|
2275
|
+
};
|
|
2276
|
+
a.on("close", closeOther(b));
|
|
2277
|
+
b.on("close", closeOther(a));
|
|
2278
|
+
a.pipe(b);
|
|
2279
|
+
b.pipe(a);
|
|
2280
|
+
}
|
|
2281
|
+
async function startGatewayForwarder(options) {
|
|
2282
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
2283
|
+
const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
2284
|
+
const refreshBeforeMs = options.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
|
|
2285
|
+
const now = options.now ?? Date.now;
|
|
2286
|
+
const emit = (event) => {
|
|
2287
|
+
try {
|
|
2288
|
+
options.onEvent?.(event);
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
let certificate = options.certificate;
|
|
2293
|
+
let running = false;
|
|
2294
|
+
let stopped = false;
|
|
2295
|
+
let connectionId = 0;
|
|
2296
|
+
let refreshTimer;
|
|
2297
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
2298
|
+
const track = (socket) => {
|
|
2299
|
+
sockets.add(socket);
|
|
2300
|
+
socket.once("close", () => sockets.delete(socket));
|
|
2301
|
+
socket.on("error", () => socket.destroy());
|
|
2302
|
+
};
|
|
2303
|
+
async function openUpstream(cert, client) {
|
|
2304
|
+
const raw = netConnect({ host: cert.host, port: cert.port });
|
|
2305
|
+
track(raw);
|
|
2306
|
+
raw.setNoDelay(true);
|
|
2307
|
+
await waitFor(raw, "connect");
|
|
2308
|
+
const rawReader = new PacketReader(raw);
|
|
2309
|
+
const greetingPacket = await rawReader.read();
|
|
2310
|
+
const greeting = parseGreeting(greetingPacket.payload);
|
|
2311
|
+
if ((greeting.capabilities & CLIENT.SSL) === 0) throw new Error("the gateway did not offer TLS");
|
|
2312
|
+
const capabilities = upstreamCapabilities(client.capabilities, client.database);
|
|
2313
|
+
let seq = greetingPacket.seq + 1;
|
|
2314
|
+
raw.write(encodePacket(seq, encodeSslRequest(capabilities, client.maxPacketSize, client.charset)));
|
|
2315
|
+
rawReader.release({ pause: false });
|
|
2316
|
+
const socket = tlsConnect({
|
|
2317
|
+
socket: raw,
|
|
2318
|
+
secureContext: cert.secureContext,
|
|
2319
|
+
host: cert.host,
|
|
2320
|
+
// SNI carries host names only; an IP is still verified against the certificate.
|
|
2321
|
+
...isIP(cert.host) === 0 ? { servername: cert.host } : {},
|
|
2322
|
+
rejectUnauthorized: true
|
|
2323
|
+
});
|
|
2324
|
+
track(socket);
|
|
2325
|
+
socket.setNoDelay(true);
|
|
2326
|
+
await waitFor(socket, "secureConnect");
|
|
2327
|
+
const reader = new PacketReader(socket);
|
|
2328
|
+
let plugin = PLUGIN_CACHING_SHA2;
|
|
2329
|
+
seq++;
|
|
2330
|
+
socket.write(
|
|
2331
|
+
encodePacket(
|
|
2332
|
+
seq,
|
|
2333
|
+
encodeHandshakeResponse({
|
|
2334
|
+
capabilities,
|
|
2335
|
+
maxPacketSize: client.maxPacketSize,
|
|
2336
|
+
charset: client.charset,
|
|
2337
|
+
username: options.username,
|
|
2338
|
+
authResponse: scrambleCachingSha2(options.password, greeting.scramble),
|
|
2339
|
+
database: client.database,
|
|
2340
|
+
authPlugin: plugin,
|
|
2341
|
+
attributes: client.attributes
|
|
2342
|
+
})
|
|
2343
|
+
)
|
|
2344
|
+
);
|
|
2345
|
+
for (let round = 0; round < 6; round++) {
|
|
2346
|
+
const packet = await reader.read();
|
|
2347
|
+
seq = packet.seq;
|
|
2348
|
+
const payload = packet.payload;
|
|
2349
|
+
if (isOk(payload)) return { socket, ok: payload, leftover: reader.release({ pause: true }) };
|
|
2350
|
+
if (isErr(payload)) throw serverErrorFromPacket(payload);
|
|
2351
|
+
if (isAuthSwitch(payload)) {
|
|
2352
|
+
const request = parseAuthSwitchRequest(payload);
|
|
2353
|
+
plugin = request.plugin;
|
|
2354
|
+
seq++;
|
|
2355
|
+
socket.write(encodePacket(seq, scrambleFor(plugin, options.password, request.scramble)));
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
if (isAuthMoreData(payload) && plugin === PLUGIN_CACHING_SHA2 && payload.length >= 2) {
|
|
2359
|
+
if (payload[1] === FAST_AUTH_SUCCESS) continue;
|
|
2360
|
+
if (payload[1] === PERFORM_FULL_AUTH) {
|
|
2361
|
+
seq++;
|
|
2362
|
+
socket.write(encodePacket(seq, Buffer.concat([Buffer.from(options.password, "utf8"), Buffer.from([0])])));
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
throw new Error(`unexpected packet 0x${(payload[0] ?? 0).toString(16)} from the gateway during authentication`);
|
|
2367
|
+
}
|
|
2368
|
+
throw new Error("the gateway's authentication did not finish");
|
|
2369
|
+
}
|
|
2370
|
+
async function authenticateLocal(local, reader) {
|
|
2371
|
+
const scramble = newScramble();
|
|
2372
|
+
connectionId = connectionId + 1 >>> 0;
|
|
2373
|
+
local.write(encodePacket(0, encodeGreeting(localGreeting(connectionId, scramble, SERVER_VERSION))));
|
|
2374
|
+
const first = await reader.read();
|
|
2375
|
+
let seq = first.seq;
|
|
2376
|
+
const client = parseHandshakeResponse(first.payload);
|
|
2377
|
+
if (first.payload.length === 32) {
|
|
2378
|
+
throw new LocalRefusal(ER_HANDSHAKE, "08S01", "The Meridian forwarder on this port does not use TLS; connect without it (the connection never leaves this machine).");
|
|
2379
|
+
}
|
|
2380
|
+
if ((client.capabilities & (CLIENT.COMPRESS | CLIENT.ZSTD_COMPRESSION)) !== 0) {
|
|
2381
|
+
throw new LocalRefusal(ER_HANDSHAKE, "08S01", "Compression is not supported by the Meridian database gateway; turn it off in your client.");
|
|
2382
|
+
}
|
|
2383
|
+
if (client.username !== options.username) throw accessDenied(client.username);
|
|
2384
|
+
let authResponse = client.authResponse;
|
|
2385
|
+
let plugin = client.authPlugin;
|
|
2386
|
+
if (plugin === "" && (client.capabilities & CLIENT.SECURE_CONNECTION) !== 0) plugin = PLUGIN_NATIVE;
|
|
2387
|
+
if (plugin !== PLUGIN_NATIVE && plugin !== PLUGIN_CACHING_SHA2) {
|
|
2388
|
+
seq++;
|
|
2389
|
+
local.write(encodePacket(seq, encodeAuthSwitchRequest(PLUGIN_NATIVE, scramble)));
|
|
2390
|
+
const answer = await reader.read();
|
|
2391
|
+
seq = answer.seq;
|
|
2392
|
+
authResponse = answer.payload;
|
|
2393
|
+
plugin = PLUGIN_NATIVE;
|
|
2394
|
+
}
|
|
2395
|
+
const expected = plugin === PLUGIN_CACHING_SHA2 ? scrambleCachingSha2(options.password, scramble) : scrambleNative(options.password, scramble);
|
|
2396
|
+
if (!scrambleMatches(authResponse, expected)) throw accessDenied(client.username);
|
|
2397
|
+
if (plugin === PLUGIN_CACHING_SHA2) {
|
|
2398
|
+
seq++;
|
|
2399
|
+
local.write(encodePacket(seq, Buffer.from([AUTH_MORE_DATA, FAST_AUTH_SUCCESS])));
|
|
2400
|
+
}
|
|
2401
|
+
return { client, seq };
|
|
2402
|
+
}
|
|
2403
|
+
async function handleConnection(local) {
|
|
2404
|
+
track(local);
|
|
2405
|
+
local.setNoDelay(true);
|
|
2406
|
+
const reader = new PacketReader(local);
|
|
2407
|
+
let upstream;
|
|
2408
|
+
const timer = setTimeout(() => {
|
|
2409
|
+
emit({ type: "connection-error", message: "A local MySQL handshake timed out." });
|
|
2410
|
+
local.destroy();
|
|
2411
|
+
upstream?.destroy();
|
|
2412
|
+
}, handshakeTimeoutMs);
|
|
2413
|
+
timer.unref();
|
|
2414
|
+
let seq = 0;
|
|
2415
|
+
try {
|
|
2416
|
+
const authenticated = await authenticateLocal(local, reader);
|
|
2417
|
+
seq = authenticated.seq;
|
|
2418
|
+
const cert = certificate;
|
|
2419
|
+
let session;
|
|
2420
|
+
try {
|
|
2421
|
+
session = await openUpstream(cert, authenticated.client);
|
|
2422
|
+
} catch (error2) {
|
|
2423
|
+
if (error2 instanceof MysqlServerError) {
|
|
2424
|
+
throw new LocalRefusal(error2.code, error2.sqlState, error2.message);
|
|
2425
|
+
}
|
|
2426
|
+
throw new LocalRefusal(
|
|
2427
|
+
CR_CONN_HOST_ERROR,
|
|
2428
|
+
"HY000",
|
|
2429
|
+
`Cannot reach the Meridian database gateway at ${cert.host}:${cert.port}: ${describeError(error2)}`
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
upstream = session.socket;
|
|
2433
|
+
clearTimeout(timer);
|
|
2434
|
+
local.write(encodePacket(seq + 1, session.ok));
|
|
2435
|
+
const pipelined = reader.release({ pause: true });
|
|
2436
|
+
if (pipelined.length > 0) upstream.write(pipelined);
|
|
2437
|
+
if (session.leftover.length > 0) local.write(session.leftover);
|
|
2438
|
+
splice(local, upstream);
|
|
2439
|
+
} catch (error2) {
|
|
2440
|
+
clearTimeout(timer);
|
|
2441
|
+
upstream?.destroy();
|
|
2442
|
+
if (error2 instanceof LocalRefusal) {
|
|
2443
|
+
if (error2.code !== ER_ACCESS_DENIED) emit({ type: "connection-error", message: error2.message });
|
|
2444
|
+
if (!local.destroyed) local.end(encodePacket(seq + 1, encodeErr(error2.code, error2.sqlState, error2.message)));
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
emit({ type: "connection-error", message: describeError(error2) });
|
|
2448
|
+
local.destroy();
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
async function refresh() {
|
|
2452
|
+
if (options.refreshCertificate === void 0) return;
|
|
2453
|
+
const next = await options.refreshCertificate();
|
|
2454
|
+
certificate = next;
|
|
2455
|
+
emit({ type: "certificate-refreshed", expiresAt: next.expiresAt });
|
|
2456
|
+
}
|
|
2457
|
+
function scheduleRefresh(delayMs, attempt) {
|
|
2458
|
+
if (options.refreshCertificate === void 0 || stopped) return;
|
|
2459
|
+
refreshTimer = setTimeout(() => {
|
|
2460
|
+
refresh().then(
|
|
2461
|
+
() => scheduleRefresh(certificateRefreshDelay(certificate.expiresAt, now(), refreshBeforeMs), 0),
|
|
2462
|
+
(error2) => {
|
|
2463
|
+
const backoff = REFRESH_RETRY_BACKOFFS_MS[Math.min(attempt, REFRESH_RETRY_BACKOFFS_MS.length - 1)] ?? 3e5;
|
|
2464
|
+
emit({
|
|
2465
|
+
type: "error",
|
|
2466
|
+
message: `Could not renew the database gateway certificate (${describeError(error2)}); retrying in ${Math.round(backoff / 1e3)}s.`
|
|
2467
|
+
});
|
|
2468
|
+
scheduleRefresh(backoff, attempt + 1);
|
|
2469
|
+
}
|
|
2470
|
+
);
|
|
2471
|
+
}, delayMs);
|
|
2472
|
+
refreshTimer.unref();
|
|
2473
|
+
}
|
|
2474
|
+
const server = createServer((local) => {
|
|
2475
|
+
void handleConnection(local);
|
|
2476
|
+
});
|
|
2477
|
+
await new Promise((resolve2, reject) => {
|
|
2478
|
+
server.once("error", (error2) => {
|
|
2479
|
+
reject(
|
|
2480
|
+
error2.code === "EADDRINUSE" ? new CliError(`Port ${options.port} is already in use.`, {
|
|
2481
|
+
hint: "Pick a different port with --port, or free up that port and try again."
|
|
2482
|
+
}) : error2
|
|
2483
|
+
);
|
|
2484
|
+
});
|
|
2485
|
+
server.listen({ port: options.port, host, exclusive: true }, () => resolve2());
|
|
2486
|
+
});
|
|
2487
|
+
server.on("error", (error2) => emit({ type: "error", message: describeError(error2) }));
|
|
2488
|
+
const address = server.address();
|
|
2489
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port;
|
|
2490
|
+
running = true;
|
|
2491
|
+
emit({ type: "ready", port });
|
|
2492
|
+
scheduleRefresh(certificateRefreshDelay(certificate.expiresAt, now(), refreshBeforeMs), 0);
|
|
2493
|
+
return {
|
|
2494
|
+
host,
|
|
2495
|
+
port,
|
|
2496
|
+
get running() {
|
|
2497
|
+
return running;
|
|
2498
|
+
},
|
|
2499
|
+
get certificate() {
|
|
2500
|
+
return certificate;
|
|
2501
|
+
},
|
|
2502
|
+
refresh,
|
|
2503
|
+
async stop() {
|
|
2504
|
+
if (stopped) return;
|
|
2505
|
+
stopped = true;
|
|
2506
|
+
running = false;
|
|
2507
|
+
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
2508
|
+
const closed = new Promise((resolve2) => server.close(() => resolve2()));
|
|
2509
|
+
for (const socket of sockets) socket.destroy();
|
|
2510
|
+
await closed;
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1745
2515
|
// src/lib/port.ts
|
|
1746
|
-
import { createServer } from "net";
|
|
2516
|
+
import { createServer as createServer2 } from "net";
|
|
1747
2517
|
var DEFAULT_START_PORT = 3307;
|
|
1748
2518
|
var DEFAULT_ATTEMPTS = 20;
|
|
1749
2519
|
var MAX_PORT = 65535;
|
|
1750
2520
|
function isPortFree(port, host = "127.0.0.1") {
|
|
1751
2521
|
return new Promise((resolve2) => {
|
|
1752
|
-
const server =
|
|
2522
|
+
const server = createServer2();
|
|
1753
2523
|
server.once("error", () => {
|
|
1754
2524
|
resolve2(false);
|
|
1755
2525
|
});
|
|
@@ -1761,7 +2531,7 @@ function isPortFree(port, host = "127.0.0.1") {
|
|
|
1761
2531
|
}
|
|
1762
2532
|
function bindEphemeralPort(host) {
|
|
1763
2533
|
return new Promise((resolve2) => {
|
|
1764
|
-
const server =
|
|
2534
|
+
const server = createServer2();
|
|
1765
2535
|
server.once("error", () => resolve2(null));
|
|
1766
2536
|
server.listen({ port: 0, host, exclusive: true }, () => {
|
|
1767
2537
|
const address = server.address();
|
|
@@ -1784,7 +2554,7 @@ async function findFreePort(options = {}) {
|
|
|
1784
2554
|
}
|
|
1785
2555
|
const ephemeral = await bindEphemeralPort(host);
|
|
1786
2556
|
if (ephemeral === null) {
|
|
1787
|
-
throw new CliError("Could not find a free local port for the
|
|
2557
|
+
throw new CliError("Could not find a free local port for the database gateway forwarder.", {
|
|
1788
2558
|
hint: "Close other applications using local ports and try again."
|
|
1789
2559
|
});
|
|
1790
2560
|
}
|
|
@@ -1997,513 +2767,6 @@ function diagnoseMigrationFailure(root) {
|
|
|
1997
2767
|
return offender === null ? null : foreignDialectExplanation(offender);
|
|
1998
2768
|
}
|
|
1999
2769
|
|
|
2000
|
-
// src/lib/proxy/binary.ts
|
|
2001
|
-
import { createHash } from "crypto";
|
|
2002
|
-
import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
2003
|
-
import { join as join4 } from "path";
|
|
2004
|
-
var PROXY_VERSION = "2.24.1";
|
|
2005
|
-
var PROXY_CHECKSUMS = Object.freeze({
|
|
2006
|
-
"cloud-sql-proxy.darwin.amd64": "67a38444d03fe77a8b11036f96fe5d2379640088bb6e338737fb48d97c58cf48",
|
|
2007
|
-
"cloud-sql-proxy.darwin.arm64": "fd374545705f2aa687d8e525a2efcbb9d356cc8af60d4dfa1a330cb59a7f9450",
|
|
2008
|
-
"cloud-sql-proxy.linux.386": "cf731f4becfb5a2de61f2998b2d660c5fb8f3067cbd3b86f5783ae9049c06f24",
|
|
2009
|
-
"cloud-sql-proxy.linux.amd64": "fae2766aac9d614a2bdef2f2a7778f3d054f3acd5ff07a81a9e300bd471512eb",
|
|
2010
|
-
"cloud-sql-proxy.linux.arm64": "da49fd73b0d33c0ab8e5dfc19dfb104cdd969ebd355eef8fd3b079c20a723c82",
|
|
2011
|
-
"cloud-sql-proxy.x64.exe": "01ec54c69dbb8118c0b181c33ff1469a5cdb2dddb49eb86b0b173f6c55dea0bc",
|
|
2012
|
-
"cloud-sql-proxy.x86.exe": "98c3b1897c80f2bc0b241b4452c18557a00cd86f89e4d7449c4ff1316e243c44"
|
|
2013
|
-
});
|
|
2014
|
-
var ASSET_NAME_BY_PLATFORM_ARCH = Object.freeze({
|
|
2015
|
-
"darwin:arm64": "cloud-sql-proxy.darwin.arm64",
|
|
2016
|
-
"darwin:x64": "cloud-sql-proxy.darwin.amd64",
|
|
2017
|
-
"linux:x64": "cloud-sql-proxy.linux.amd64",
|
|
2018
|
-
"linux:arm64": "cloud-sql-proxy.linux.arm64",
|
|
2019
|
-
"linux:ia32": "cloud-sql-proxy.linux.386",
|
|
2020
|
-
"win32:x64": "cloud-sql-proxy.x64.exe",
|
|
2021
|
-
"win32:ia32": "cloud-sql-proxy.x86.exe"
|
|
2022
|
-
});
|
|
2023
|
-
function defaultVersion() {
|
|
2024
|
-
const override = process.env.MERIDIAN_PROXY_VERSION;
|
|
2025
|
-
if (override !== void 0 && override !== "") return override;
|
|
2026
|
-
return PROXY_VERSION;
|
|
2027
|
-
}
|
|
2028
|
-
function resolveProxyAsset(options) {
|
|
2029
|
-
const platform = options?.platform ?? process.platform;
|
|
2030
|
-
const arch = options?.arch ?? process.arch;
|
|
2031
|
-
const version = options?.version ?? defaultVersion();
|
|
2032
|
-
const assetName = ASSET_NAME_BY_PLATFORM_ARCH[`${platform}:${arch}`];
|
|
2033
|
-
if (assetName === void 0) {
|
|
2034
|
-
throw new CliError(`The Cloud SQL Auth Proxy has no published build for ${platform}/${arch}.`, {
|
|
2035
|
-
hint: "Set MERIDIAN_PROXY_PATH to the path of a proxy binary you install yourself."
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
const downloadUrl = `https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v${version}/${assetName}`;
|
|
2039
|
-
const sha256Value = version === PROXY_VERSION ? PROXY_CHECKSUMS[assetName] : void 0;
|
|
2040
|
-
const fileName = `cloud-sql-proxy-${version}-${platform}-${arch}${platform === "win32" ? ".exe" : ""}`;
|
|
2041
|
-
return {
|
|
2042
|
-
version,
|
|
2043
|
-
assetName,
|
|
2044
|
-
downloadUrl,
|
|
2045
|
-
...sha256Value !== void 0 ? { sha256: sha256Value } : {},
|
|
2046
|
-
fileName
|
|
2047
|
-
};
|
|
2048
|
-
}
|
|
2049
|
-
function sha256(data) {
|
|
2050
|
-
return createHash("sha256").update(data).digest("hex");
|
|
2051
|
-
}
|
|
2052
|
-
var CHECKSUM_ROW_PATTERN = /\[([^\]]+)]\([^)]*\)\s*\|\s*([0-9a-fA-F]{64})/g;
|
|
2053
|
-
function parseChecksumTable(releaseBody) {
|
|
2054
|
-
const table = {};
|
|
2055
|
-
for (const match of releaseBody.matchAll(CHECKSUM_ROW_PATTERN)) {
|
|
2056
|
-
const name = match[1];
|
|
2057
|
-
const hash = match[2];
|
|
2058
|
-
if (name !== void 0 && hash !== void 0) table[name] = hash.toLowerCase();
|
|
2059
|
-
}
|
|
2060
|
-
return table;
|
|
2061
|
-
}
|
|
2062
|
-
var RELEASE_API_URL = "https://api.github.com/repos/GoogleCloudPlatform/cloud-sql-proxy/releases/tags";
|
|
2063
|
-
var checksumCache = /* @__PURE__ */ new Map();
|
|
2064
|
-
async function resolveExpectedChecksum(asset, fetchImpl) {
|
|
2065
|
-
if (asset.sha256 !== void 0) return asset.sha256;
|
|
2066
|
-
const cacheKey = `${asset.version}:${asset.assetName}`;
|
|
2067
|
-
const cached = checksumCache.get(cacheKey);
|
|
2068
|
-
if (cached !== void 0) return cached;
|
|
2069
|
-
const skipAllowed = process.env.MERIDIAN_PROXY_SKIP_CHECKSUM === "1";
|
|
2070
|
-
const url = `${RELEASE_API_URL}/v${asset.version}`;
|
|
2071
|
-
let response;
|
|
2072
|
-
try {
|
|
2073
|
-
response = await fetchImpl(url, { headers: { Accept: "application/vnd.github+json" } });
|
|
2074
|
-
} catch (error2) {
|
|
2075
|
-
if (skipAllowed) return void 0;
|
|
2076
|
-
throw new CliError(`Could not reach ${url} to look up the checksum for cloud-sql-proxy v${asset.version}.`, {
|
|
2077
|
-
hint: "Check your network connection, or set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk.",
|
|
2078
|
-
cause: error2
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
if (!response.ok) {
|
|
2082
|
-
if (skipAllowed) return void 0;
|
|
2083
|
-
throw new CliError(
|
|
2084
|
-
`Could not determine the sha256 checksum for cloud-sql-proxy v${asset.version} (GitHub release lookup for ${url} returned HTTP ${response.status}).`,
|
|
2085
|
-
{
|
|
2086
|
-
hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk."
|
|
2087
|
-
}
|
|
2088
|
-
);
|
|
2089
|
-
}
|
|
2090
|
-
const rawBody = await response.text();
|
|
2091
|
-
let releaseNotes = rawBody;
|
|
2092
|
-
try {
|
|
2093
|
-
const parsed = JSON.parse(rawBody);
|
|
2094
|
-
if (typeof parsed === "object" && parsed !== null && "body" in parsed && typeof parsed.body === "string") {
|
|
2095
|
-
releaseNotes = parsed.body;
|
|
2096
|
-
}
|
|
2097
|
-
} catch {
|
|
2098
|
-
}
|
|
2099
|
-
const checksum = parseChecksumTable(releaseNotes)[asset.assetName];
|
|
2100
|
-
if (checksum !== void 0) {
|
|
2101
|
-
checksumCache.set(cacheKey, checksum);
|
|
2102
|
-
return checksum;
|
|
2103
|
-
}
|
|
2104
|
-
if (skipAllowed) return void 0;
|
|
2105
|
-
throw new CliError(
|
|
2106
|
-
`Could not find a published sha256 checksum for ${asset.assetName} in the cloud-sql-proxy v${asset.version} release notes.`,
|
|
2107
|
-
{ hint: "Set MERIDIAN_PROXY_SKIP_CHECKSUM=1 to install without verifying the checksum, at your own risk." }
|
|
2108
|
-
);
|
|
2109
|
-
}
|
|
2110
|
-
function parseContentLength(header) {
|
|
2111
|
-
if (header === null) return void 0;
|
|
2112
|
-
const value = Number(header);
|
|
2113
|
-
return Number.isFinite(value) ? value : void 0;
|
|
2114
|
-
}
|
|
2115
|
-
async function readResponseBody(response, onProgress) {
|
|
2116
|
-
const total = parseContentLength(response.headers.get("content-length"));
|
|
2117
|
-
if (response.body === null) {
|
|
2118
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
2119
|
-
onProgress?.(buffer.byteLength, total);
|
|
2120
|
-
return buffer;
|
|
2121
|
-
}
|
|
2122
|
-
const reader = response.body.getReader();
|
|
2123
|
-
const chunks = [];
|
|
2124
|
-
let received = 0;
|
|
2125
|
-
for (; ; ) {
|
|
2126
|
-
const { done, value } = await reader.read();
|
|
2127
|
-
if (done) break;
|
|
2128
|
-
if (value !== void 0) {
|
|
2129
|
-
chunks.push(value);
|
|
2130
|
-
received += value.byteLength;
|
|
2131
|
-
onProgress?.(received, total);
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
return Buffer.concat(chunks);
|
|
2135
|
-
}
|
|
2136
|
-
async function ensureProxyBinary(options = {}) {
|
|
2137
|
-
const overridePath = process.env.MERIDIAN_PROXY_PATH;
|
|
2138
|
-
if (overridePath !== void 0 && overridePath !== "") {
|
|
2139
|
-
if (!existsSync4(overridePath)) {
|
|
2140
|
-
throw new CliError(`MERIDIAN_PROXY_PATH is set to ${overridePath}, but that file does not exist.`, {
|
|
2141
|
-
hint: "Fix or unset MERIDIAN_PROXY_PATH."
|
|
2142
|
-
});
|
|
2143
|
-
}
|
|
2144
|
-
return overridePath;
|
|
2145
|
-
}
|
|
2146
|
-
const asset = resolveProxyAsset(options.version !== void 0 ? { version: options.version } : {});
|
|
2147
|
-
const dir = options.dir ?? proxyBinDir();
|
|
2148
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2149
|
-
const targetPath = join4(dir, asset.fileName);
|
|
2150
|
-
mkdirSync4(dir, { recursive: true });
|
|
2151
|
-
if (!options.force && existsSync4(targetPath)) {
|
|
2152
|
-
const cached = readFileSync6(targetPath);
|
|
2153
|
-
const expected2 = await resolveExpectedChecksum(asset, fetchImpl);
|
|
2154
|
-
if (expected2 === void 0 || sha256(cached) === expected2) return targetPath;
|
|
2155
|
-
rmSync2(targetPath, { force: true });
|
|
2156
|
-
}
|
|
2157
|
-
const response = await fetchImpl(asset.downloadUrl);
|
|
2158
|
-
if (!response.ok) {
|
|
2159
|
-
throw new CliError(
|
|
2160
|
-
`Failed to download the Cloud SQL Auth Proxy v${asset.version} from ${asset.downloadUrl} (HTTP ${response.status}).`,
|
|
2161
|
-
{
|
|
2162
|
-
hint: "Check your network connection (and any corporate proxy settings), or set MERIDIAN_PROXY_PATH to a proxy binary you install yourself."
|
|
2163
|
-
}
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
const buffer = await readResponseBody(response, options.onProgress);
|
|
2167
|
-
const expected = await resolveExpectedChecksum(asset, fetchImpl);
|
|
2168
|
-
const actual = sha256(buffer);
|
|
2169
|
-
if (expected !== void 0 && actual !== expected) {
|
|
2170
|
-
throw new CliError(
|
|
2171
|
-
`Checksum mismatch for ${asset.assetName}: expected ${expected.slice(0, 12)}..., got ${actual.slice(0, 12)}....`,
|
|
2172
|
-
{
|
|
2173
|
-
hint: "This can happen after a corrupted download or an untrusted network. Try again; if it persists, report this."
|
|
2174
|
-
}
|
|
2175
|
-
);
|
|
2176
|
-
}
|
|
2177
|
-
const tmpPath = join4(dir, `.${asset.fileName}.${process.pid}.tmp`);
|
|
2178
|
-
writeFileSync5(tmpPath, buffer);
|
|
2179
|
-
chmodSync3(tmpPath, 493);
|
|
2180
|
-
renameSync3(tmpPath, targetPath);
|
|
2181
|
-
return targetPath;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// src/lib/proxy/proxy.ts
|
|
2185
|
-
import { spawn } from "child_process";
|
|
2186
|
-
import { createConnection } from "net";
|
|
2187
|
-
var PROBE_INTERVAL_MS = 150;
|
|
2188
|
-
var KILL_GRACE_MS = 2e3;
|
|
2189
|
-
var CRASH_BACKOFFS_MS = [200, 1e3, 3e3];
|
|
2190
|
-
var MAX_CRASH_RESTARTS = CRASH_BACKOFFS_MS.length;
|
|
2191
|
-
var REFRESH_RETRY_BACKOFFS_MS = [3e4, 6e4, 12e4];
|
|
2192
|
-
var MIN_REFRESH_DELAY_MS = 6e4;
|
|
2193
|
-
var LOG_RING_SIZE = 20;
|
|
2194
|
-
var STDERR_TAIL_LINES = 10;
|
|
2195
|
-
function buildProxyArgs(input2) {
|
|
2196
|
-
return ["--address", input2.host, "--port", String(input2.port), input2.instanceConnectionName];
|
|
2197
|
-
}
|
|
2198
|
-
function buildProxyEnv(accessToken, base = process.env) {
|
|
2199
|
-
return { ...base, CSQL_PROXY_TOKEN: accessToken };
|
|
2200
|
-
}
|
|
2201
|
-
function defaultProbe(host, port) {
|
|
2202
|
-
return new Promise((resolve2) => {
|
|
2203
|
-
const socket = createConnection({ host, port });
|
|
2204
|
-
const finish = (ok) => {
|
|
2205
|
-
socket.removeAllListeners();
|
|
2206
|
-
socket.destroy();
|
|
2207
|
-
resolve2(ok);
|
|
2208
|
-
};
|
|
2209
|
-
socket.once("connect", () => finish(true));
|
|
2210
|
-
socket.once("error", () => finish(false));
|
|
2211
|
-
socket.setTimeout(1e3, () => finish(false));
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
function computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs) {
|
|
2215
|
-
let expiresAtMs;
|
|
2216
|
-
if (token.expires_at !== void 0) {
|
|
2217
|
-
const parsed = new Date(token.expires_at).getTime();
|
|
2218
|
-
if (!Number.isNaN(parsed)) expiresAtMs = parsed;
|
|
2219
|
-
}
|
|
2220
|
-
const effectiveExpiryMs = expiresAtMs ?? Date.now() + assumedLifetimeMs;
|
|
2221
|
-
return Math.max(effectiveExpiryMs - Date.now() - refreshSkewMs, MIN_REFRESH_DELAY_MS);
|
|
2222
|
-
}
|
|
2223
|
-
function buildStartupFailureError(code, signal, lastLines) {
|
|
2224
|
-
const tail = lastLines.slice(-STDERR_TAIL_LINES).join("\n");
|
|
2225
|
-
const lower = tail.toLowerCase();
|
|
2226
|
-
let hint2 = "Run `meridian dev` again; if this keeps happening, check the instance connection name and your network.";
|
|
2227
|
-
if (lower.includes("iam") || lower.includes("permission") || lower.includes("forbidden")) {
|
|
2228
|
-
hint2 = "The Meridian API's service account may be missing roles/cloudsql.client on the dev Cloud SQL instance. Contact Meridian support.";
|
|
2229
|
-
} else if (lower.includes("invalid token") || lower.includes("401") || lower.includes("unauthorized")) {
|
|
2230
|
-
hint2 = "The access token appears to have expired. Retry `meridian dev` to mint a fresh one.";
|
|
2231
|
-
} else if (lower.includes("address already in use")) {
|
|
2232
|
-
hint2 = "Another process is already listening on that port. Free it up, or retry with a different port.";
|
|
2233
|
-
}
|
|
2234
|
-
const summary = `The Cloud SQL Auth Proxy exited before it was ready (code ${code ?? "null"}, signal ${signal ?? "null"}).`;
|
|
2235
|
-
return new CliError(tail === "" ? summary : `${summary}
|
|
2236
|
-
${tail}`, { hint: hint2 });
|
|
2237
|
-
}
|
|
2238
|
-
function waitForReady(child, probeImpl, host, port, timeoutMs, setTimeoutImpl, getLastLines) {
|
|
2239
|
-
return new Promise((resolve2, reject) => {
|
|
2240
|
-
let settled = false;
|
|
2241
|
-
let pollTimer;
|
|
2242
|
-
let timeoutTimer;
|
|
2243
|
-
const cleanup = () => {
|
|
2244
|
-
if (pollTimer !== void 0) clearTimeout(pollTimer);
|
|
2245
|
-
if (timeoutTimer !== void 0) clearTimeout(timeoutTimer);
|
|
2246
|
-
child.off("exit", onExit);
|
|
2247
|
-
};
|
|
2248
|
-
function onExit(code, signal) {
|
|
2249
|
-
if (settled) return;
|
|
2250
|
-
settled = true;
|
|
2251
|
-
cleanup();
|
|
2252
|
-
reject(buildStartupFailureError(code, signal, getLastLines()));
|
|
2253
|
-
}
|
|
2254
|
-
function poll() {
|
|
2255
|
-
if (settled) return;
|
|
2256
|
-
probeImpl(host, port).then((ok) => {
|
|
2257
|
-
if (settled) return;
|
|
2258
|
-
if (ok) {
|
|
2259
|
-
settled = true;
|
|
2260
|
-
cleanup();
|
|
2261
|
-
resolve2();
|
|
2262
|
-
} else {
|
|
2263
|
-
pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
|
|
2264
|
-
}
|
|
2265
|
-
}).catch(() => {
|
|
2266
|
-
if (!settled) pollTimer = setTimeoutImpl(poll, PROBE_INTERVAL_MS);
|
|
2267
|
-
});
|
|
2268
|
-
}
|
|
2269
|
-
child.on("exit", onExit);
|
|
2270
|
-
timeoutTimer = setTimeoutImpl(() => {
|
|
2271
|
-
if (settled) return;
|
|
2272
|
-
settled = true;
|
|
2273
|
-
cleanup();
|
|
2274
|
-
reject(
|
|
2275
|
-
new CliError(`Timed out after ${timeoutMs}ms waiting for the Cloud SQL Auth Proxy to accept connections.`, {
|
|
2276
|
-
hint: "The instance may be slow to reach, or readyTimeoutMs may need to be raised."
|
|
2277
|
-
})
|
|
2278
|
-
);
|
|
2279
|
-
}, timeoutMs);
|
|
2280
|
-
poll();
|
|
2281
|
-
});
|
|
2282
|
-
}
|
|
2283
|
-
async function startProxy(options) {
|
|
2284
|
-
const host = options.host ?? "127.0.0.1";
|
|
2285
|
-
const spawnImpl = options.spawnImpl ?? spawn;
|
|
2286
|
-
const probeImpl = options.probeImpl ?? defaultProbe;
|
|
2287
|
-
const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
|
|
2288
|
-
const refreshSkewMs = options.refreshSkewMs ?? 5 * 6e4;
|
|
2289
|
-
const assumedLifetimeMs = options.assumedLifetimeMs ?? 55 * 6e4;
|
|
2290
|
-
const readyTimeoutMs = options.readyTimeoutMs ?? 3e4;
|
|
2291
|
-
const emit = (event) => options.onEvent?.(event);
|
|
2292
|
-
let stopped = false;
|
|
2293
|
-
let running = false;
|
|
2294
|
-
let expectingExit = false;
|
|
2295
|
-
let currentToken = options.token;
|
|
2296
|
-
let child;
|
|
2297
|
-
let refreshTimer;
|
|
2298
|
-
let crashTimer;
|
|
2299
|
-
let refreshBackoffIndex = 0;
|
|
2300
|
-
let crashRestartsRemaining = MAX_CRASH_RESTARTS;
|
|
2301
|
-
let attemptCounter = 0;
|
|
2302
|
-
let cleanupFns = [];
|
|
2303
|
-
const knownTokens = /* @__PURE__ */ new Set();
|
|
2304
|
-
const logRing = [];
|
|
2305
|
-
function trackToken(token) {
|
|
2306
|
-
if (token.access_token !== "") knownTokens.add(token.access_token);
|
|
2307
|
-
}
|
|
2308
|
-
function redact(text) {
|
|
2309
|
-
let result = text;
|
|
2310
|
-
for (const token of knownTokens) result = result.split(token).join("<redacted>");
|
|
2311
|
-
return result;
|
|
2312
|
-
}
|
|
2313
|
-
function pushLog(line) {
|
|
2314
|
-
const safeLine = redact(line);
|
|
2315
|
-
logRing.push(safeLine);
|
|
2316
|
-
if (logRing.length > LOG_RING_SIZE) logRing.shift();
|
|
2317
|
-
emit({ type: "log", line: safeLine });
|
|
2318
|
-
}
|
|
2319
|
-
function wireOutput(c) {
|
|
2320
|
-
for (const stream of [c.stdout, c.stderr]) {
|
|
2321
|
-
if (stream === null) continue;
|
|
2322
|
-
let buffer = "";
|
|
2323
|
-
const onData = (chunk) => {
|
|
2324
|
-
buffer += chunk.toString();
|
|
2325
|
-
const lines = buffer.split("\n");
|
|
2326
|
-
buffer = lines.pop() ?? "";
|
|
2327
|
-
for (const line of lines) pushLog(line);
|
|
2328
|
-
};
|
|
2329
|
-
stream.on("data", onData);
|
|
2330
|
-
cleanupFns.push(() => stream.off("data", onData));
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
function wireExit(c) {
|
|
2334
|
-
c.once("exit", (code, signal) => {
|
|
2335
|
-
if (child !== c) return;
|
|
2336
|
-
const wasRunning = running;
|
|
2337
|
-
const wasExpected = expectingExit;
|
|
2338
|
-
running = false;
|
|
2339
|
-
expectingExit = false;
|
|
2340
|
-
emit({ type: "exit", code, signal });
|
|
2341
|
-
if (stopped || wasExpected || !wasRunning) return;
|
|
2342
|
-
void handleCrash();
|
|
2343
|
-
});
|
|
2344
|
-
}
|
|
2345
|
-
async function spawnChild() {
|
|
2346
|
-
attemptCounter += 1;
|
|
2347
|
-
emit({ type: "starting", attempt: attemptCounter });
|
|
2348
|
-
const args = buildProxyArgs({
|
|
2349
|
-
instanceConnectionName: options.instanceConnectionName,
|
|
2350
|
-
host,
|
|
2351
|
-
port: options.port
|
|
2352
|
-
});
|
|
2353
|
-
const c = spawnImpl(options.binaryPath, args, {
|
|
2354
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2355
|
-
env: buildProxyEnv(currentToken.access_token, options.env)
|
|
2356
|
-
});
|
|
2357
|
-
child = c;
|
|
2358
|
-
wireOutput(c);
|
|
2359
|
-
wireExit(c);
|
|
2360
|
-
try {
|
|
2361
|
-
await waitForReady(c, probeImpl, host, options.port, readyTimeoutMs, setTimeoutImpl, () => logRing.slice());
|
|
2362
|
-
} catch (error2) {
|
|
2363
|
-
expectingExit = true;
|
|
2364
|
-
await killChild(c);
|
|
2365
|
-
if (child === c) child = void 0;
|
|
2366
|
-
throw error2;
|
|
2367
|
-
}
|
|
2368
|
-
emit({ type: "ready", port: options.port });
|
|
2369
|
-
}
|
|
2370
|
-
function killChild(c) {
|
|
2371
|
-
if (c.exitCode !== null || c.signalCode !== null) return Promise.resolve();
|
|
2372
|
-
return new Promise((resolve2) => {
|
|
2373
|
-
let settled = false;
|
|
2374
|
-
let killTimer;
|
|
2375
|
-
const onExit = () => {
|
|
2376
|
-
if (settled) return;
|
|
2377
|
-
settled = true;
|
|
2378
|
-
if (killTimer !== void 0) clearTimeout(killTimer);
|
|
2379
|
-
resolve2();
|
|
2380
|
-
};
|
|
2381
|
-
c.once("exit", onExit);
|
|
2382
|
-
c.kill("SIGTERM");
|
|
2383
|
-
killTimer = setTimeoutImpl(() => {
|
|
2384
|
-
if (!settled) c.kill("SIGKILL");
|
|
2385
|
-
}, KILL_GRACE_MS);
|
|
2386
|
-
});
|
|
2387
|
-
}
|
|
2388
|
-
function scheduleRefresh(token) {
|
|
2389
|
-
if (stopped || options.refreshToken === void 0) return;
|
|
2390
|
-
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
2391
|
-
const delayMs = computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs);
|
|
2392
|
-
refreshTimer = setTimeoutImpl(() => {
|
|
2393
|
-
refreshTimer = void 0;
|
|
2394
|
-
void handleRefreshFire();
|
|
2395
|
-
}, delayMs);
|
|
2396
|
-
}
|
|
2397
|
-
function scheduleRefreshRetry() {
|
|
2398
|
-
if (stopped) return;
|
|
2399
|
-
const delayMs = REFRESH_RETRY_BACKOFFS_MS[Math.min(refreshBackoffIndex, REFRESH_RETRY_BACKOFFS_MS.length - 1)] ?? 12e4;
|
|
2400
|
-
refreshBackoffIndex += 1;
|
|
2401
|
-
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
2402
|
-
refreshTimer = setTimeoutImpl(() => {
|
|
2403
|
-
refreshTimer = void 0;
|
|
2404
|
-
void handleRefreshFire();
|
|
2405
|
-
}, delayMs);
|
|
2406
|
-
}
|
|
2407
|
-
async function handleRefreshFire() {
|
|
2408
|
-
if (stopped || options.refreshToken === void 0) return;
|
|
2409
|
-
let newToken;
|
|
2410
|
-
try {
|
|
2411
|
-
newToken = await options.refreshToken();
|
|
2412
|
-
} catch (error2) {
|
|
2413
|
-
emit({ type: "error", message: redact(`Token refresh failed: ${describeError(error2)}`) });
|
|
2414
|
-
scheduleRefreshRetry();
|
|
2415
|
-
return;
|
|
2416
|
-
}
|
|
2417
|
-
if (stopped) return;
|
|
2418
|
-
trackToken(newToken);
|
|
2419
|
-
currentToken = newToken;
|
|
2420
|
-
expectingExit = true;
|
|
2421
|
-
emit({ type: "restarting", reason: "token-refresh" });
|
|
2422
|
-
try {
|
|
2423
|
-
const previous = child;
|
|
2424
|
-
if (previous !== void 0) await killChild(previous);
|
|
2425
|
-
await spawnChild();
|
|
2426
|
-
} catch (error2) {
|
|
2427
|
-
emit({ type: "error", message: redact(`Restart after token refresh failed: ${describeError(error2)}`) });
|
|
2428
|
-
scheduleRefreshRetry();
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
running = true;
|
|
2432
|
-
refreshBackoffIndex = 0;
|
|
2433
|
-
emit({ type: "token-refreshed", ...newToken.expires_at !== void 0 ? { expiresAt: newToken.expires_at } : {} });
|
|
2434
|
-
scheduleRefresh(newToken);
|
|
2435
|
-
}
|
|
2436
|
-
async function handleCrash() {
|
|
2437
|
-
if (stopped) return;
|
|
2438
|
-
if (crashRestartsRemaining <= 0) {
|
|
2439
|
-
const tail = logRing.slice(-STDERR_TAIL_LINES).join("\n");
|
|
2440
|
-
emit({
|
|
2441
|
-
type: "error",
|
|
2442
|
-
message: `The Cloud SQL Auth Proxy crashed repeatedly and could not be restarted.${tail === "" ? "" : `
|
|
2443
|
-
${tail}`}`
|
|
2444
|
-
});
|
|
2445
|
-
return;
|
|
2446
|
-
}
|
|
2447
|
-
const attemptIndex = MAX_CRASH_RESTARTS - crashRestartsRemaining;
|
|
2448
|
-
const delayMs = CRASH_BACKOFFS_MS[attemptIndex] ?? (CRASH_BACKOFFS_MS[CRASH_BACKOFFS_MS.length - 1] ?? 3e3);
|
|
2449
|
-
crashRestartsRemaining -= 1;
|
|
2450
|
-
emit({ type: "restarting", reason: "crash" });
|
|
2451
|
-
crashTimer = setTimeoutImpl(() => {
|
|
2452
|
-
crashTimer = void 0;
|
|
2453
|
-
if (stopped) return;
|
|
2454
|
-
spawnChild().then(() => {
|
|
2455
|
-
running = true;
|
|
2456
|
-
}).catch((error2) => {
|
|
2457
|
-
emit({ type: "error", message: redact(describeError(error2)) });
|
|
2458
|
-
void handleCrash();
|
|
2459
|
-
});
|
|
2460
|
-
}, delayMs);
|
|
2461
|
-
}
|
|
2462
|
-
async function refresh() {
|
|
2463
|
-
if (stopped) return;
|
|
2464
|
-
if (options.refreshToken === void 0) {
|
|
2465
|
-
throw new CliError("Cannot refresh the Cloud SQL Auth Proxy token: no refreshToken() was configured.");
|
|
2466
|
-
}
|
|
2467
|
-
if (refreshTimer !== void 0) {
|
|
2468
|
-
clearTimeout(refreshTimer);
|
|
2469
|
-
refreshTimer = void 0;
|
|
2470
|
-
}
|
|
2471
|
-
refreshBackoffIndex = 0;
|
|
2472
|
-
await handleRefreshFire();
|
|
2473
|
-
}
|
|
2474
|
-
async function stop() {
|
|
2475
|
-
if (stopped) return;
|
|
2476
|
-
stopped = true;
|
|
2477
|
-
if (refreshTimer !== void 0) clearTimeout(refreshTimer);
|
|
2478
|
-
refreshTimer = void 0;
|
|
2479
|
-
if (crashTimer !== void 0) clearTimeout(crashTimer);
|
|
2480
|
-
crashTimer = void 0;
|
|
2481
|
-
for (const cleanup of cleanupFns) cleanup();
|
|
2482
|
-
cleanupFns = [];
|
|
2483
|
-
const current2 = child;
|
|
2484
|
-
if (current2 !== void 0) {
|
|
2485
|
-
expectingExit = true;
|
|
2486
|
-
await killChild(current2);
|
|
2487
|
-
}
|
|
2488
|
-
running = false;
|
|
2489
|
-
child = void 0;
|
|
2490
|
-
}
|
|
2491
|
-
trackToken(currentToken);
|
|
2492
|
-
await spawnChild();
|
|
2493
|
-
running = true;
|
|
2494
|
-
scheduleRefresh(currentToken);
|
|
2495
|
-
return {
|
|
2496
|
-
host,
|
|
2497
|
-
port: options.port,
|
|
2498
|
-
instanceConnectionName: options.instanceConnectionName,
|
|
2499
|
-
get running() {
|
|
2500
|
-
return running;
|
|
2501
|
-
},
|
|
2502
|
-
refresh,
|
|
2503
|
-
stop
|
|
2504
|
-
};
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
2770
|
// src/lib/signals.ts
|
|
2508
2771
|
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2509
2772
|
var SIGNAL_EXIT_CODES = {
|
|
@@ -2594,8 +2857,8 @@ function defaultSleep(ms) {
|
|
|
2594
2857
|
}
|
|
2595
2858
|
function resolveDeps(overrides) {
|
|
2596
2859
|
return {
|
|
2597
|
-
|
|
2598
|
-
|
|
2860
|
+
issueGatewayCertificate,
|
|
2861
|
+
startGatewayForwarder,
|
|
2599
2862
|
findFreePort,
|
|
2600
2863
|
patchEnvFile,
|
|
2601
2864
|
waitForDatabase,
|
|
@@ -2666,15 +2929,6 @@ async function ensureDevDatabaseReady(client, appId, options = {}) {
|
|
|
2666
2929
|
{ success: (db) => `Developer database "${db.database}" is ready.` }
|
|
2667
2930
|
);
|
|
2668
2931
|
}
|
|
2669
|
-
function toProxyToken(token) {
|
|
2670
|
-
return {
|
|
2671
|
-
access_token: token.access_token,
|
|
2672
|
-
...typeof token.expires_at === "string" ? { expires_at: token.expires_at } : {}
|
|
2673
|
-
};
|
|
2674
|
-
}
|
|
2675
|
-
function formatMb(bytes) {
|
|
2676
|
-
return (bytes / (1024 * 1024)).toFixed(1);
|
|
2677
|
-
}
|
|
2678
2932
|
function firstNonEmpty(...values) {
|
|
2679
2933
|
for (const value of values) {
|
|
2680
2934
|
if (typeof value === "string" && value.trim() !== "") return value;
|
|
@@ -2696,32 +2950,53 @@ function logEnvPatch(result, databaseUrl, wroteShadow) {
|
|
|
2696
2950
|
}
|
|
2697
2951
|
if (result.backedUp) detail(`Backed up the previous file to ${result.path}.backup`);
|
|
2698
2952
|
}
|
|
2699
|
-
function
|
|
2953
|
+
async function pickLocalPort(options, deps) {
|
|
2954
|
+
const findPortOptions = { start: DEFAULT_PORT_START };
|
|
2955
|
+
if (options.port !== void 0) findPortOptions.preferred = options.port;
|
|
2956
|
+
const port = await deps.findFreePort(findPortOptions);
|
|
2957
|
+
if (options.port !== void 0 && port !== options.port) {
|
|
2958
|
+
throw new CliError(`Port ${options.port} is already in use.`, {
|
|
2959
|
+
hint: "Pick a different port with --port, or free up that port and try again."
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
return port;
|
|
2963
|
+
}
|
|
2964
|
+
function handleForwarderEvent(event) {
|
|
2700
2965
|
switch (event.type) {
|
|
2701
|
-
case "starting":
|
|
2702
|
-
debug(`Starting the Cloud SQL Auth Proxy (attempt ${event.attempt}).`);
|
|
2703
|
-
return;
|
|
2704
2966
|
case "ready":
|
|
2705
|
-
debug(`
|
|
2967
|
+
debug(`Database gateway forwarder listening on port ${event.port}.`);
|
|
2706
2968
|
return;
|
|
2707
|
-
case "
|
|
2708
|
-
debug(event.
|
|
2969
|
+
case "connection-error":
|
|
2970
|
+
debug(`Database gateway connection failed: ${event.message}`);
|
|
2709
2971
|
return;
|
|
2710
|
-
case "
|
|
2711
|
-
step(
|
|
2712
|
-
return;
|
|
2713
|
-
case "restarting":
|
|
2714
|
-
if (event.reason === "crash") warn("The Cloud SQL Auth Proxy crashed; restarting it.");
|
|
2715
|
-
else debug("Restarting the Cloud SQL Auth Proxy with a refreshed token...");
|
|
2716
|
-
return;
|
|
2717
|
-
case "exit":
|
|
2718
|
-
debug(`Cloud SQL Auth Proxy exited (code ${event.code ?? "null"}, signal ${event.signal ?? "null"}).`);
|
|
2972
|
+
case "certificate-refreshed":
|
|
2973
|
+
step(`Renewed the database gateway certificate (expires ${event.expiresAt}).`);
|
|
2719
2974
|
return;
|
|
2720
2975
|
case "error":
|
|
2721
|
-
warn(`
|
|
2976
|
+
warn(`Database gateway: ${event.message} Restart \`meridian dev\` if the database becomes unreachable.`);
|
|
2722
2977
|
return;
|
|
2723
2978
|
}
|
|
2724
2979
|
}
|
|
2980
|
+
async function startGatewayEndpoint(client, appId, credentials, options, deps) {
|
|
2981
|
+
const certificate = await withSpinner(
|
|
2982
|
+
"Requesting a database gateway certificate...",
|
|
2983
|
+
() => deps.issueGatewayCertificate(client, appId),
|
|
2984
|
+
{ success: (cert) => `Issued a database gateway certificate (expires ${cert.expiresAt}).` }
|
|
2985
|
+
);
|
|
2986
|
+
const port = await pickLocalPort(options, deps);
|
|
2987
|
+
return withSpinner(
|
|
2988
|
+
`Starting the database gateway forwarder on 127.0.0.1:${port}...`,
|
|
2989
|
+
() => deps.startGatewayForwarder({
|
|
2990
|
+
port,
|
|
2991
|
+
username: credentials.username,
|
|
2992
|
+
password: credentials.password,
|
|
2993
|
+
certificate,
|
|
2994
|
+
...options.refreshToken !== false ? { refreshCertificate: () => deps.issueGatewayCertificate(client, appId) } : {},
|
|
2995
|
+
onEvent: handleForwarderEvent
|
|
2996
|
+
}),
|
|
2997
|
+
{ success: `Database gateway forwarder listening on 127.0.0.1:${port} (${certificate.host}).` }
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
2725
3000
|
async function startDevSession(options = {}) {
|
|
2726
3001
|
const deps = resolveDeps(options.deps);
|
|
2727
3002
|
let proxy;
|
|
@@ -2738,55 +3013,8 @@ async function startDevSession(options = {}) {
|
|
|
2738
3013
|
success: (creds) => `Fetched credentials for database "${creds.database}"${typeof creds.expires_at === "string" ? ` (expire ${creds.expires_at})` : ""}.`
|
|
2739
3014
|
}
|
|
2740
3015
|
);
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
() => client.issueProxyToken(appId),
|
|
2744
|
-
{ success: "Issued a Cloud SQL Auth Proxy token." }
|
|
2745
|
-
);
|
|
2746
|
-
const binaryPath = await withSpinner(
|
|
2747
|
-
"Preparing the Cloud SQL Auth Proxy...",
|
|
2748
|
-
(spinner2) => {
|
|
2749
|
-
const ensureOptions = {
|
|
2750
|
-
onProgress: (received, total) => {
|
|
2751
|
-
spinner2.update(
|
|
2752
|
-
total !== void 0 ? `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)}/${formatMb(total)} MB)` : `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)} MB)`
|
|
2753
|
-
);
|
|
2754
|
-
}
|
|
2755
|
-
};
|
|
2756
|
-
return deps.ensureProxyBinary(ensureOptions);
|
|
2757
|
-
},
|
|
2758
|
-
{ success: "Cloud SQL Auth Proxy is ready." }
|
|
2759
|
-
);
|
|
2760
|
-
const findPortOptions = { start: DEFAULT_PORT_START };
|
|
2761
|
-
if (options.port !== void 0) findPortOptions.preferred = options.port;
|
|
2762
|
-
const port = await deps.findFreePort(findPortOptions);
|
|
2763
|
-
if (options.port !== void 0 && port !== options.port) {
|
|
2764
|
-
throw new CliError(`Port ${options.port} is already in use.`, {
|
|
2765
|
-
hint: "Pick a different port with --port, or free up that port and try again."
|
|
2766
|
-
});
|
|
2767
|
-
}
|
|
2768
|
-
const instanceConnectionName = firstNonEmpty(
|
|
2769
|
-
credentials.instance_connection_name,
|
|
2770
|
-
proxyToken.instance_connection_name,
|
|
2771
|
-
devDatabase.instance_connection_name
|
|
2772
|
-
);
|
|
2773
|
-
if (instanceConnectionName === void 0) {
|
|
2774
|
-
throw new CliError("The Meridian API did not return a Cloud SQL instance connection name.", {
|
|
2775
|
-
hint: "Try again, or contact Meridian support if this persists."
|
|
2776
|
-
});
|
|
2777
|
-
}
|
|
2778
|
-
proxy = await withSpinner(
|
|
2779
|
-
`Starting the Cloud SQL Auth Proxy on 127.0.0.1:${port}...`,
|
|
2780
|
-
() => deps.startProxy({
|
|
2781
|
-
binaryPath,
|
|
2782
|
-
instanceConnectionName,
|
|
2783
|
-
token: toProxyToken(proxyToken),
|
|
2784
|
-
port,
|
|
2785
|
-
...options.refreshToken !== false ? { refreshToken: async () => toProxyToken(await client.issueProxyToken(appId)) } : {},
|
|
2786
|
-
onEvent: handleProxyEvent
|
|
2787
|
-
}),
|
|
2788
|
-
{ success: `Cloud SQL Auth Proxy listening on 127.0.0.1:${port}.` }
|
|
2789
|
-
);
|
|
3016
|
+
const access = "gateway";
|
|
3017
|
+
proxy = await startGatewayEndpoint(client, appId, credentials, options, deps);
|
|
2790
3018
|
const connection = connectionInfoFromCredentials(credentials, { host: proxy.host, port: proxy.port });
|
|
2791
3019
|
if (options.waitForDb !== false) {
|
|
2792
3020
|
await withSpinner(
|
|
@@ -2838,6 +3066,7 @@ async function startDevSession(options = {}) {
|
|
|
2838
3066
|
connection,
|
|
2839
3067
|
databaseUrl,
|
|
2840
3068
|
...shadowDatabaseUrl !== void 0 ? { shadowDatabaseUrl } : {},
|
|
3069
|
+
access,
|
|
2841
3070
|
proxy,
|
|
2842
3071
|
env: {
|
|
2843
3072
|
...process.env,
|
|
@@ -3086,7 +3315,7 @@ function registerDbCredentialCommand(db) {
|
|
|
3086
3315
|
|
|
3087
3316
|
// src/commands/db-shell-repl.ts
|
|
3088
3317
|
import { accessSync, constants as fsConstants } from "fs";
|
|
3089
|
-
import { delimiter as delimiter2, join as
|
|
3318
|
+
import { delimiter as delimiter2, join as join4 } from "path";
|
|
3090
3319
|
import { createInterface } from "readline";
|
|
3091
3320
|
function candidateNames(name) {
|
|
3092
3321
|
if (process.platform !== "win32") return [name];
|
|
@@ -3101,7 +3330,7 @@ function findExecutable(name) {
|
|
|
3101
3330
|
const candidates = candidateNames(name);
|
|
3102
3331
|
for (const dir of dirs) {
|
|
3103
3332
|
for (const candidate of candidates) {
|
|
3104
|
-
const fullPath =
|
|
3333
|
+
const fullPath = join4(dir, candidate);
|
|
3105
3334
|
try {
|
|
3106
3335
|
accessSync(fullPath, fsConstants.X_OK);
|
|
3107
3336
|
return fullPath;
|
|
@@ -3236,7 +3465,7 @@ function defaultDbDeps() {
|
|
|
3236
3465
|
function hasSeedConfigured(root) {
|
|
3237
3466
|
let parsed;
|
|
3238
3467
|
try {
|
|
3239
|
-
parsed = JSON.parse(
|
|
3468
|
+
parsed = JSON.parse(readFileSync6(join5(root, "package.json"), "utf8"));
|
|
3240
3469
|
} catch {
|
|
3241
3470
|
return false;
|
|
3242
3471
|
}
|
|
@@ -3415,7 +3644,7 @@ function registerDbCommand(program) {
|
|
|
3415
3644
|
db.action(() => {
|
|
3416
3645
|
db.outputHelp();
|
|
3417
3646
|
});
|
|
3418
|
-
db.command("reset").description("Drop and recreate every table in the developer database (keeps the MySQL user).").option("--migrate", "run `npx prisma migrate deploy` afterwards, without asking").option("--seed", "run `npx prisma db seed` afterwards, without asking").option("--port <port>", "local port for the
|
|
3647
|
+
db.command("reset").description("Drop and recreate every table in the developer database (keeps the MySQL user).").option("--migrate", "run `npx prisma migrate deploy` afterwards, without asking").option("--seed", "run `npx prisma db seed` afterwards, without asking").option("--port <port>", "local port for the database gateway forwarder", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").action(async (opts) => {
|
|
3419
3648
|
await runAndSetExitCode2(
|
|
3420
3649
|
() => runDbReset({
|
|
3421
3650
|
...opts.migrate === true ? { migrate: true } : {},
|
|
@@ -3425,7 +3654,7 @@ function registerDbCommand(program) {
|
|
|
3425
3654
|
})
|
|
3426
3655
|
);
|
|
3427
3656
|
});
|
|
3428
|
-
db.command("seed").description("Run `npx prisma db seed` with the
|
|
3657
|
+
db.command("seed").description("Run `npx prisma db seed` with the database connection up and DATABASE_URL set.").option("--port <port>", "local port for the database gateway forwarder", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").passThroughOptions().argument("[args...]", "extra arguments forwarded to `npx prisma db seed` (put them after `--`)").action(async (args, opts) => {
|
|
3429
3658
|
await runAndSetExitCode2(
|
|
3430
3659
|
() => runDbSeed({
|
|
3431
3660
|
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
@@ -3434,7 +3663,7 @@ function registerDbCommand(program) {
|
|
|
3434
3663
|
})
|
|
3435
3664
|
);
|
|
3436
3665
|
});
|
|
3437
|
-
db.command("shell").description("Open a SQL shell against the developer database (the `mysql` client if available, otherwise a built-in REPL).").option("--port <port>", "local port for the
|
|
3666
|
+
db.command("shell").description("Open a SQL shell against the developer database (the `mysql` client if available, otherwise a built-in REPL).").option("--port <port>", "local port for the database gateway forwarder", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to read/patch (defaults to .env)").option("--repl", "force the built-in SQL REPL even when `mysql` is on PATH").option("-e, --execute <sql>", "run one SQL statement and exit").option("--no-env", "do not patch DATABASE_URL into the project's .env file").action(async (opts) => {
|
|
3438
3667
|
await runAndSetExitCode2(
|
|
3439
3668
|
() => runDbShell({
|
|
3440
3669
|
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
@@ -3506,7 +3735,7 @@ function printDevSummary(session, noEnv) {
|
|
|
3506
3735
|
const entries = [
|
|
3507
3736
|
["App", appLabel(session.project)],
|
|
3508
3737
|
["Database", session.devDatabase.database],
|
|
3509
|
-
["Proxy", `127.0.0.1:${session.proxy.port}`],
|
|
3738
|
+
[session.access === "gateway" ? "Gateway" : "Proxy", `127.0.0.1:${session.proxy.port}`],
|
|
3510
3739
|
["Env file", envEntry],
|
|
3511
3740
|
["DATABASE_URL", redactDatabaseUrl(session.databaseUrl)]
|
|
3512
3741
|
];
|
|
@@ -3521,6 +3750,7 @@ function buildDevJsonPayload(session) {
|
|
|
3521
3750
|
return {
|
|
3522
3751
|
app: appLabel(session.project),
|
|
3523
3752
|
database: session.devDatabase.database,
|
|
3753
|
+
access: session.access,
|
|
3524
3754
|
proxy: { host: session.proxy.host, port: session.proxy.port },
|
|
3525
3755
|
database_url: redactDatabaseUrl(session.databaseUrl),
|
|
3526
3756
|
env_file: session.envPatch?.path ?? null,
|
|
@@ -3572,7 +3802,7 @@ async function runDev(options = {}) {
|
|
|
3572
3802
|
}
|
|
3573
3803
|
}
|
|
3574
3804
|
function registerDevCommand(program) {
|
|
3575
|
-
program.command("dev").description("Run the managed developer database proxy and your app's dev server together.").option("--port <port>", "preferred local port for the
|
|
3805
|
+
program.command("dev").description("Run the managed developer database proxy and your app's dev server together.").option("--port <port>", "preferred local port for the database gateway forwarder").option("--env-file <file>", "the .env file to patch, relative to the project root (default .env)").option("--no-env", "don't touch any .env file; just print the DATABASE_URL for manual use").argument("[command...]", "run this instead of the app's dev script, e.g. `meridian dev -- next dev`").passThroughOptions().action(async (command, opts) => {
|
|
3576
3806
|
const port = parsePortOption(opts.port);
|
|
3577
3807
|
const code = await runDev({
|
|
3578
3808
|
...command.length > 0 ? { command } : {},
|
|
@@ -3585,8 +3815,8 @@ function registerDevCommand(program) {
|
|
|
3585
3815
|
}
|
|
3586
3816
|
|
|
3587
3817
|
// src/commands/link.ts
|
|
3588
|
-
import { existsSync as
|
|
3589
|
-
import { join as
|
|
3818
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3819
|
+
import { join as join6 } from "path";
|
|
3590
3820
|
function defaultDeps3() {
|
|
3591
3821
|
return { createClient: createApiClient };
|
|
3592
3822
|
}
|
|
@@ -3605,7 +3835,7 @@ async function runLink(options = {}) {
|
|
|
3605
3835
|
const deps = { ...defaultDeps3(), ...options.deps };
|
|
3606
3836
|
const json = isJsonOutput();
|
|
3607
3837
|
const root = resolveProjectRoot(workingDirectory()) ?? workingDirectory();
|
|
3608
|
-
if (!
|
|
3838
|
+
if (!existsSync4(join6(root, "package.json"))) {
|
|
3609
3839
|
log(`No package.json found in ${style.path(root)}.`);
|
|
3610
3840
|
const proceed = await confirmAction({
|
|
3611
3841
|
message: "Link this directory to a Meridian app anyway?",
|
|
@@ -3919,7 +4149,7 @@ function registerLogoutCommand(program) {
|
|
|
3919
4149
|
|
|
3920
4150
|
// src/lib/db/snapshot.ts
|
|
3921
4151
|
import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
|
|
3922
|
-
import { join as
|
|
4152
|
+
import { join as join7 } from "path";
|
|
3923
4153
|
function snapshotFileName(date = /* @__PURE__ */ new Date()) {
|
|
3924
4154
|
return `${date.toISOString().replace(/[:.]/g, "-")}.sql`;
|
|
3925
4155
|
}
|
|
@@ -3938,7 +4168,7 @@ async function createSnapshot(info, options) {
|
|
|
3938
4168
|
}
|
|
3939
4169
|
const dump = options.dumpImpl ?? await loadMysqldump();
|
|
3940
4170
|
await mkdir(options.dir, { recursive: true });
|
|
3941
|
-
const path =
|
|
4171
|
+
const path = join7(options.dir, snapshotFileName(createdAt));
|
|
3942
4172
|
try {
|
|
3943
4173
|
await dump({
|
|
3944
4174
|
connection: {
|
|
@@ -4144,7 +4374,7 @@ async function listSnapshots(dir) {
|
|
|
4144
4374
|
const names = entries.filter((name) => name.endsWith(".sql")).sort().reverse();
|
|
4145
4375
|
const results = [];
|
|
4146
4376
|
for (const name of names) {
|
|
4147
|
-
const path =
|
|
4377
|
+
const path = join7(dir, name);
|
|
4148
4378
|
const { size } = await stat(path);
|
|
4149
4379
|
results.push({ path, name, bytes: size, createdAt: parseSnapshotDate(name) });
|
|
4150
4380
|
}
|
|
@@ -4340,7 +4570,7 @@ function registerMigrateCommand(program) {
|
|
|
4340
4570
|
).option("--skip-snapshot", "run without a pre-migration snapshot (no rollback is possible)").option(
|
|
4341
4571
|
"--allow-foreign-migrations",
|
|
4342
4572
|
"run even when prisma/migrations was generated for SQLite or Postgres and cannot apply to MySQL"
|
|
4343
|
-
).option("--no-rollback", "keep a snapshot but do not automatically restore it after a failed migration").option("--keep-snapshots <n>", "number of recent snapshots to keep after a successful migration", "5").option("--port <port>", "preferred local port for the
|
|
4573
|
+
).option("--no-rollback", "keep a snapshot but do not automatically restore it after a failed migration").option("--keep-snapshots <n>", "number of recent snapshots to keep after a successful migration", "5").option("--port <port>", "preferred local port for the database gateway forwarder").option("--env-file <file>", "path (relative to the project root) of the .env file to patch with DATABASE_URL").argument("[prismaArgs...]", "extra arguments forwarded to `prisma migrate dev` verbatim, after --").passThroughOptions().action(async (prismaArgs, opts) => {
|
|
4344
4574
|
const keepSnapshots = parseIntegerOption(opts.keepSnapshots, "--keep-snapshots", { min: 0 });
|
|
4345
4575
|
const port = parsePortOption(opts.port);
|
|
4346
4576
|
const exitCode = await runMigrate({
|