@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/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 readFileSync7 } from "fs";
10
- import { join as join6 } from "path";
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
- async issueProxyToken(appId) {
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
- proxyTokenSchema,
354
+ devDatabaseCertificateSchema,
341
355
  "POST",
342
- `/app/cli/apps/${encodePathSegment(appId)}/dev-database/proxy-token`
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: createConnection2 } = await import("mysql2/promise");
722
- const connection = await createConnection2({
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 Cloud SQL Auth Proxy for you; start it first if you are connecting by hand."
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 = createServer();
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 = createServer();
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 Cloud SQL Auth Proxy.", {
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
- ensureProxyBinary,
2598
- startProxy,
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 handleProxyEvent(event) {
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(`Cloud SQL Auth Proxy listening on port ${event.port}.`);
2967
+ debug(`Database gateway forwarder listening on port ${event.port}.`);
2706
2968
  return;
2707
- case "log":
2708
- debug(event.line);
2969
+ case "connection-error":
2970
+ debug(`Database gateway connection failed: ${event.message}`);
2709
2971
  return;
2710
- case "token-refreshed":
2711
- step("Refreshed the Cloud SQL Auth Proxy token.");
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(`Cloud SQL Auth Proxy error: ${event.message} Restart \`meridian dev\` if the database becomes unreachable.`);
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 proxyToken = await withSpinner(
2742
- "Requesting a Cloud SQL Auth Proxy token...",
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 join5 } from "path";
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 = join5(dir, candidate);
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(readFileSync7(join6(root, "package.json"), "utf8"));
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 Cloud SQL Auth Proxy", (value) => parsePortOption(value)).option("--env-file <file>", "project .env file to update (defaults to .env)").action(async (opts) => {
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 Cloud SQL Auth Proxy up and DATABASE_URL set.").option("--port <port>", "local port for the Cloud SQL Auth Proxy", (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) => {
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 Cloud SQL Auth Proxy", (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) => {
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 Cloud SQL Auth Proxy").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) => {
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 existsSync5 } from "fs";
3589
- import { join as join7 } from "path";
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 (!existsSync5(join7(root, "package.json"))) {
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 join8 } from "path";
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 = join8(options.dir, snapshotFileName(createdAt));
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 = join8(dir, name);
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 Cloud SQL Auth Proxy").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) => {
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({