@the-meridian/cli 1.1.0 → 1.2.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
@@ -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: `gateway` (the Meridian database
85
+ * gateway with a per-developer certificate) or `proxy` (cloud-sql-proxy).
86
+ * Absent from an API older than the gateway, which means `proxy`.
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(),
@@ -342,6 +357,18 @@ var ApiClient = class _ApiClient {
342
357
  `/app/cli/apps/${encodePathSegment(appId)}/dev-database/proxy-token`
343
358
  );
344
359
  }
360
+ /**
361
+ * Signs `publicKeyPem` (SPKI) as this developer's client certificate for the
362
+ * database gateway. Each call replaces the previous certificate server-side.
363
+ */
364
+ async issueDevDatabaseCertificate(appId, publicKeyPem) {
365
+ return this.request(
366
+ devDatabaseCertificateSchema,
367
+ "POST",
368
+ `/app/cli/apps/${encodePathSegment(appId)}/dev-database/certificate`,
369
+ { body: { public_key: publicKeyPem } }
370
+ );
371
+ }
345
372
  async resetDevDatabase(appId) {
346
373
  return this.request(devDatabaseSchema, "POST", `/app/cli/apps/${encodePathSegment(appId)}/dev-database/reset`);
347
374
  }
@@ -523,7 +550,9 @@ function extractErrorCode(body) {
523
550
  }
524
551
  var HINTS_BY_ERROR_CODE = {
525
552
  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.",
553
+ dev_databases_unavailable: "This Meridian environment does not offer developer databases. Run `meridian dev` against production (the default API URL) instead.",
526
554
  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.",
555
+ cli_upgrade_required: "Upgrade the Meridian CLI: npm i -g @the-meridian/cli",
527
556
  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
557
  FEATURE_NOT_ENTITLED: "Your organization's plan does not include managed developer databases; upgrade it in the Meridian dashboard."
529
558
  };
@@ -1742,14 +1771,770 @@ function redactDatabaseUrl(url) {
1742
1771
  return url.replace(/^(mysql:\/\/[^:@/]+):([^@]*)@/, "$1:****@");
1743
1772
  }
1744
1773
 
1774
+ // src/lib/gateway/certificate.ts
1775
+ import { generateKeyPair } from "crypto";
1776
+ import { createSecureContext } from "tls";
1777
+ import { promisify } from "util";
1778
+ var generateKeyPairAsync = promisify(generateKeyPair);
1779
+ function gatewayCertificateFromPem(material) {
1780
+ return {
1781
+ host: material.host,
1782
+ port: material.port,
1783
+ expiresAt: material.expiresAt,
1784
+ fingerprint: material.fingerprint,
1785
+ secureContext: createSecureContext({
1786
+ key: material.keyPem,
1787
+ cert: material.certificatePem,
1788
+ ca: material.caPem,
1789
+ minVersion: "TLSv1.2"
1790
+ })
1791
+ };
1792
+ }
1793
+ async function generateGatewayKeyPair() {
1794
+ const { publicKey, privateKey } = await generateKeyPairAsync("ec", { namedCurve: "prime256v1" });
1795
+ return {
1796
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
1797
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString()
1798
+ };
1799
+ }
1800
+ async function issueGatewayCertificate(client, appId) {
1801
+ const { publicKeyPem, privateKeyPem } = await generateGatewayKeyPair();
1802
+ const issued = await client.issueDevDatabaseCertificate(appId, publicKeyPem);
1803
+ return gatewayCertificateFromPem({
1804
+ keyPem: privateKeyPem,
1805
+ certificatePem: issued.certificate_pem,
1806
+ caPem: issued.ca_pem,
1807
+ host: issued.host,
1808
+ port: issued.port,
1809
+ expiresAt: issued.expires_at,
1810
+ fingerprint: issued.fingerprint
1811
+ });
1812
+ }
1813
+ var ASSUMED_LIFETIME_MS = 24 * 60 * 6e4;
1814
+ var DEFAULT_REFRESH_BEFORE_MS = 4 * 60 * 6e4;
1815
+ var MIN_REFRESH_DELAY_MS = 6e4;
1816
+ function certificateRefreshDelay(expiresAt, now, refreshBeforeMs = DEFAULT_REFRESH_BEFORE_MS) {
1817
+ const parsed = Date.parse(expiresAt);
1818
+ const expiry = Number.isNaN(parsed) ? now + ASSUMED_LIFETIME_MS : parsed;
1819
+ return Math.max(MIN_REFRESH_DELAY_MS, expiry - refreshBeforeMs - now);
1820
+ }
1821
+
1822
+ // src/lib/gateway/forwarder.ts
1823
+ import { connect as netConnect, createServer, isIP } from "net";
1824
+ import { connect as tlsConnect } from "tls";
1825
+
1826
+ // src/lib/gateway/packets.ts
1827
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
1828
+ var CLIENT = {
1829
+ LONG_PASSWORD: 1,
1830
+ FOUND_ROWS: 2,
1831
+ LONG_FLAG: 4,
1832
+ CONNECT_WITH_DB: 8,
1833
+ NO_SCHEMA: 16,
1834
+ COMPRESS: 32,
1835
+ ODBC: 64,
1836
+ LOCAL_FILES: 128,
1837
+ IGNORE_SPACE: 256,
1838
+ PROTOCOL_41: 512,
1839
+ INTERACTIVE: 1024,
1840
+ SSL: 2048,
1841
+ IGNORE_SIGPIPE: 4096,
1842
+ TRANSACTIONS: 8192,
1843
+ RESERVED: 16384,
1844
+ SECURE_CONNECTION: 32768,
1845
+ MULTI_STATEMENTS: 65536,
1846
+ MULTI_RESULTS: 131072,
1847
+ PS_MULTI_RESULTS: 262144,
1848
+ PLUGIN_AUTH: 524288,
1849
+ CONNECT_ATTRS: 1048576,
1850
+ PLUGIN_AUTH_LENENC_CLIENT_DATA: 2097152,
1851
+ CAN_HANDLE_EXPIRED_PASSWORDS: 4194304,
1852
+ SESSION_TRACK: 8388608,
1853
+ DEPRECATE_EOF: 16777216,
1854
+ OPTIONAL_RESULTSET_METADATA: 33554432,
1855
+ ZSTD_COMPRESSION: 67108864,
1856
+ QUERY_ATTRIBUTES: 134217728
1857
+ };
1858
+ 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;
1859
+ var LOCAL_CAPABILITIES = GATEWAY_CAPABILITIES & ~CLIENT.SSL;
1860
+ var PLUGIN_NATIVE = "mysql_native_password";
1861
+ var PLUGIN_CACHING_SHA2 = "caching_sha2_password";
1862
+ var SCRAMBLE_LENGTH = 20;
1863
+ var DEFAULT_CHARSET = 255;
1864
+ var MAX_PAYLOAD = 16777215;
1865
+ var MAX_HANDSHAKE_PACKET = 64 * 1024;
1866
+ var AUTH_MORE_DATA = 1;
1867
+ var FAST_AUTH_SUCCESS = 3;
1868
+ var PERFORM_FULL_AUTH = 4;
1869
+ var SERVER_STATUS_AUTOCOMMIT = 2;
1870
+ function encodePacket(seq, payload) {
1871
+ if (payload.length >= MAX_PAYLOAD) throw new Error(`refusing to write a ${payload.length} byte handshake packet`);
1872
+ const header = Buffer.alloc(4);
1873
+ header.writeUIntLE(payload.length, 0, 3);
1874
+ header[3] = seq & 255;
1875
+ return Buffer.concat([header, payload]);
1876
+ }
1877
+ var PacketReader = class {
1878
+ #socket;
1879
+ #maxPacket;
1880
+ #buffer = Buffer.alloc(0);
1881
+ #failure;
1882
+ #waiter;
1883
+ #onData = (chunk) => {
1884
+ this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
1885
+ this.#settle();
1886
+ };
1887
+ #onClose = () => {
1888
+ this.#fail(new Error("the connection closed during the MySQL handshake"));
1889
+ };
1890
+ #onError = (error2) => {
1891
+ this.#fail(error2);
1892
+ };
1893
+ constructor(socket, maxPacket = MAX_HANDSHAKE_PACKET) {
1894
+ this.#socket = socket;
1895
+ this.#maxPacket = maxPacket;
1896
+ socket.on("data", this.#onData);
1897
+ socket.on("end", this.#onClose);
1898
+ socket.on("close", this.#onClose);
1899
+ socket.on("error", this.#onError);
1900
+ }
1901
+ read() {
1902
+ return new Promise((resolve2, reject) => {
1903
+ this.#waiter = { resolve: resolve2, reject };
1904
+ this.#settle();
1905
+ });
1906
+ }
1907
+ /** Stops reading. `pause` keeps later bytes in the socket for a pipe() to pick up. */
1908
+ release(options) {
1909
+ if (options.pause) this.#socket.pause();
1910
+ this.#socket.off("data", this.#onData);
1911
+ this.#socket.off("end", this.#onClose);
1912
+ this.#socket.off("close", this.#onClose);
1913
+ this.#socket.off("error", this.#onError);
1914
+ const rest = this.#buffer;
1915
+ this.#buffer = Buffer.alloc(0);
1916
+ return rest;
1917
+ }
1918
+ #fail(error2) {
1919
+ if (this.#failure === void 0) this.#failure = error2;
1920
+ this.#settle();
1921
+ }
1922
+ #settle() {
1923
+ const waiter = this.#waiter;
1924
+ if (waiter === void 0) return;
1925
+ if (this.#buffer.length >= 4) {
1926
+ const length = this.#buffer.readUIntLE(0, 3);
1927
+ if (length > this.#maxPacket) {
1928
+ this.#waiter = void 0;
1929
+ waiter.reject(new Error(`a ${length} byte packet during the MySQL handshake is too large`));
1930
+ return;
1931
+ }
1932
+ if (this.#buffer.length >= 4 + length) {
1933
+ const seq = this.#buffer[3] ?? 0;
1934
+ const payload = Buffer.from(this.#buffer.subarray(4, 4 + length));
1935
+ this.#buffer = this.#buffer.subarray(4 + length);
1936
+ this.#waiter = void 0;
1937
+ waiter.resolve({ seq, payload });
1938
+ return;
1939
+ }
1940
+ }
1941
+ if (this.#failure !== void 0) {
1942
+ this.#waiter = void 0;
1943
+ waiter.reject(this.#failure);
1944
+ }
1945
+ }
1946
+ };
1947
+ var Cursor = class {
1948
+ constructor(buf) {
1949
+ this.buf = buf;
1950
+ }
1951
+ buf;
1952
+ pos = 0;
1953
+ get remaining() {
1954
+ return this.buf.length - this.pos;
1955
+ }
1956
+ bytes(n, what) {
1957
+ if (n < 0 || this.remaining < n) throw new Error(`truncated packet reading ${what}`);
1958
+ const out = this.buf.subarray(this.pos, this.pos + n);
1959
+ this.pos += n;
1960
+ return out;
1961
+ }
1962
+ u8(what) {
1963
+ return this.bytes(1, what)[0] ?? 0;
1964
+ }
1965
+ u16(what) {
1966
+ return this.bytes(2, what).readUInt16LE(0);
1967
+ }
1968
+ u32(what) {
1969
+ return this.bytes(4, what).readUInt32LE(0);
1970
+ }
1971
+ nulString(what) {
1972
+ const end = this.buf.indexOf(0, this.pos);
1973
+ if (end < 0) throw new Error(`truncated packet reading ${what}`);
1974
+ const out = this.buf.subarray(this.pos, end);
1975
+ this.pos = end + 1;
1976
+ return out;
1977
+ }
1978
+ /** Everything left, without a trailing NUL. */
1979
+ restNul() {
1980
+ let out = this.buf.subarray(this.pos);
1981
+ this.pos = this.buf.length;
1982
+ if (out.length > 0 && out[out.length - 1] === 0) out = out.subarray(0, out.length - 1);
1983
+ return out;
1984
+ }
1985
+ lenencInt(what) {
1986
+ const first = this.u8(what);
1987
+ if (first < 251) return first;
1988
+ if (first === 252) return this.u16(what);
1989
+ if (first === 253) return this.bytes(3, what).readUIntLE(0, 3);
1990
+ if (first === 254) {
1991
+ const value = this.bytes(8, what).readBigUInt64LE(0);
1992
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${what} is too large`);
1993
+ return Number(value);
1994
+ }
1995
+ throw new Error(`invalid length-encoded integer reading ${what}`);
1996
+ }
1997
+ lenencBytes(what) {
1998
+ return this.bytes(this.lenencInt(what), what);
1999
+ }
2000
+ };
2001
+ function lenencInt(n) {
2002
+ if (n < 251) return Buffer.from([n]);
2003
+ if (n <= 65535) {
2004
+ const b2 = Buffer.alloc(3);
2005
+ b2[0] = 252;
2006
+ b2.writeUInt16LE(n, 1);
2007
+ return b2;
2008
+ }
2009
+ if (n <= 16777215) {
2010
+ const b2 = Buffer.alloc(4);
2011
+ b2[0] = 253;
2012
+ b2.writeUIntLE(n, 1, 3);
2013
+ return b2;
2014
+ }
2015
+ const b = Buffer.alloc(9);
2016
+ b[0] = 254;
2017
+ b.writeBigUInt64LE(BigInt(n), 1);
2018
+ return b;
2019
+ }
2020
+ function lenencBytes(value) {
2021
+ return Buffer.concat([lenencInt(value.length), value]);
2022
+ }
2023
+ function u16(n) {
2024
+ const b = Buffer.alloc(2);
2025
+ b.writeUInt16LE(n & 65535, 0);
2026
+ return b;
2027
+ }
2028
+ function u32(n) {
2029
+ const b = Buffer.alloc(4);
2030
+ b.writeUInt32LE(n >>> 0, 0);
2031
+ return b;
2032
+ }
2033
+ var NUL = Buffer.from([0]);
2034
+ function encodeGreeting(g) {
2035
+ if (g.scramble.length !== SCRAMBLE_LENGTH) throw new Error("the greeting scramble must be 20 bytes");
2036
+ return Buffer.concat([
2037
+ Buffer.from([10]),
2038
+ Buffer.from(g.serverVersion, "utf8"),
2039
+ NUL,
2040
+ u32(g.connectionId),
2041
+ g.scramble.subarray(0, 8),
2042
+ NUL,
2043
+ u16(g.capabilities),
2044
+ Buffer.from([g.charset]),
2045
+ u16(g.status),
2046
+ u16(g.capabilities >>> 16),
2047
+ Buffer.from([SCRAMBLE_LENGTH + 1]),
2048
+ Buffer.alloc(10),
2049
+ g.scramble.subarray(8),
2050
+ NUL,
2051
+ Buffer.from(g.authPlugin, "utf8"),
2052
+ NUL
2053
+ ]);
2054
+ }
2055
+ function localGreeting(connectionId, scramble, serverVersion) {
2056
+ return {
2057
+ serverVersion,
2058
+ connectionId,
2059
+ scramble,
2060
+ capabilities: LOCAL_CAPABILITIES,
2061
+ charset: DEFAULT_CHARSET,
2062
+ status: SERVER_STATUS_AUTOCOMMIT,
2063
+ authPlugin: PLUGIN_NATIVE
2064
+ };
2065
+ }
2066
+ function parseGreeting(payload) {
2067
+ if (payload[0] === 255) throw serverErrorFromPacket(payload);
2068
+ const r = new Cursor(payload);
2069
+ const version = r.u8("protocol version");
2070
+ if (version !== 10) throw new Error(`unsupported handshake version ${version}`);
2071
+ const serverVersion = r.nulString("server version").toString("utf8");
2072
+ const connectionId = r.u32("connection id");
2073
+ const part1 = Buffer.from(r.bytes(8, "scramble part 1"));
2074
+ r.u8("filler");
2075
+ let capabilities = r.u16("capabilities");
2076
+ let charset = DEFAULT_CHARSET;
2077
+ let status = 0;
2078
+ let authPlugin = "";
2079
+ let scramble = part1;
2080
+ if (r.remaining > 0) {
2081
+ charset = r.u8("charset");
2082
+ status = r.u16("status");
2083
+ capabilities = (capabilities | r.u16("capabilities upper") << 16) >>> 0;
2084
+ const authLength = r.u8("auth data length");
2085
+ r.bytes(10, "reserved");
2086
+ if ((capabilities & CLIENT.SECURE_CONNECTION) !== 0) {
2087
+ let part2 = r.bytes(Math.max(13, authLength - 8), "scramble part 2");
2088
+ if (part2.length > 0 && part2[part2.length - 1] === 0) part2 = part2.subarray(0, part2.length - 1);
2089
+ scramble = Buffer.concat([part1, part2]);
2090
+ }
2091
+ if ((capabilities & CLIENT.PLUGIN_AUTH) !== 0) authPlugin = r.restNul().toString("utf8");
2092
+ }
2093
+ if (scramble.length < SCRAMBLE_LENGTH) throw new Error(`the server scramble is ${scramble.length} bytes`);
2094
+ return {
2095
+ serverVersion,
2096
+ connectionId,
2097
+ scramble: scramble.subarray(0, SCRAMBLE_LENGTH),
2098
+ capabilities,
2099
+ charset,
2100
+ status,
2101
+ authPlugin: authPlugin === "" ? PLUGIN_NATIVE : authPlugin
2102
+ };
2103
+ }
2104
+ function encodeSslRequest(capabilities, maxPacketSize, charset) {
2105
+ return Buffer.concat([u32(capabilities | CLIENT.SSL), u32(maxPacketSize), Buffer.from([charset]), Buffer.alloc(23)]);
2106
+ }
2107
+ function parseHandshakeResponse(payload) {
2108
+ const r = new Cursor(payload);
2109
+ const capabilities = r.u32("capabilities");
2110
+ if ((capabilities & CLIENT.PROTOCOL_41) === 0) throw new Error("the client does not speak protocol 4.1");
2111
+ const maxPacketSize = r.u32("max packet size");
2112
+ const charset = r.u8("charset");
2113
+ r.bytes(23, "filler");
2114
+ if (r.remaining === 0) {
2115
+ return { capabilities, maxPacketSize, charset, username: "", authResponse: Buffer.alloc(0), database: "", authPlugin: "", attributes: [] };
2116
+ }
2117
+ const username = r.nulString("username").toString("utf8");
2118
+ let authResponse;
2119
+ if ((capabilities & CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA) !== 0) authResponse = r.lenencBytes("auth response");
2120
+ else if ((capabilities & CLIENT.SECURE_CONNECTION) !== 0) authResponse = r.bytes(r.u8("auth response length"), "auth response");
2121
+ else authResponse = r.nulString("auth response");
2122
+ let database = "";
2123
+ if ((capabilities & CLIENT.CONNECT_WITH_DB) !== 0 && r.remaining > 0) database = r.nulString("database").toString("utf8");
2124
+ let authPlugin = "";
2125
+ if ((capabilities & CLIENT.PLUGIN_AUTH) !== 0 && r.remaining > 0) authPlugin = r.nulString("auth plugin").toString("utf8");
2126
+ const attributes = [];
2127
+ if ((capabilities & CLIENT.CONNECT_ATTRS) !== 0 && r.remaining > 0) {
2128
+ const attrs = new Cursor(r.lenencBytes("attributes"));
2129
+ while (attrs.remaining > 0) {
2130
+ const key = attrs.lenencBytes("attribute key").toString("utf8");
2131
+ const value = attrs.lenencBytes("attribute value").toString("utf8");
2132
+ attributes.push([key, value]);
2133
+ }
2134
+ }
2135
+ return {
2136
+ capabilities,
2137
+ maxPacketSize,
2138
+ charset,
2139
+ username,
2140
+ authResponse: Buffer.from(authResponse),
2141
+ database,
2142
+ authPlugin,
2143
+ attributes
2144
+ };
2145
+ }
2146
+ function encodeHandshakeResponse(h) {
2147
+ const parts = [u32(h.capabilities), u32(h.maxPacketSize), Buffer.from([h.charset]), Buffer.alloc(23)];
2148
+ parts.push(Buffer.from(h.username, "utf8"), NUL);
2149
+ if ((h.capabilities & CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA) !== 0) parts.push(lenencBytes(h.authResponse));
2150
+ else if ((h.capabilities & CLIENT.SECURE_CONNECTION) !== 0) parts.push(Buffer.from([h.authResponse.length]), h.authResponse);
2151
+ else parts.push(h.authResponse, NUL);
2152
+ if ((h.capabilities & CLIENT.CONNECT_WITH_DB) !== 0) parts.push(Buffer.from(h.database, "utf8"), NUL);
2153
+ if ((h.capabilities & CLIENT.PLUGIN_AUTH) !== 0) parts.push(Buffer.from(h.authPlugin, "utf8"), NUL);
2154
+ if ((h.capabilities & CLIENT.CONNECT_ATTRS) !== 0) {
2155
+ const attrs = h.attributes.map(
2156
+ ([key, value]) => Buffer.concat([lenencBytes(Buffer.from(key, "utf8")), lenencBytes(Buffer.from(value, "utf8"))])
2157
+ );
2158
+ parts.push(lenencBytes(Buffer.concat(attrs)));
2159
+ }
2160
+ return Buffer.concat(parts);
2161
+ }
2162
+ function encodeAuthSwitchRequest(plugin, scramble) {
2163
+ return Buffer.concat([Buffer.from([254]), Buffer.from(plugin, "utf8"), NUL, scramble, NUL]);
2164
+ }
2165
+ function parseAuthSwitchRequest(payload) {
2166
+ if (payload[0] !== 254) throw new Error("not an auth switch request");
2167
+ const r = new Cursor(payload);
2168
+ r.pos = 1;
2169
+ const plugin = r.nulString("auth plugin").toString("utf8");
2170
+ let scramble = Buffer.from(r.restNul());
2171
+ if (scramble.length > SCRAMBLE_LENGTH) scramble = scramble.subarray(0, SCRAMBLE_LENGTH);
2172
+ return { plugin, scramble };
2173
+ }
2174
+ var isOk = (payload) => payload[0] === 0;
2175
+ var isErr = (payload) => payload[0] === 255;
2176
+ var isAuthSwitch = (payload) => payload[0] === 254;
2177
+ var isAuthMoreData = (payload) => payload[0] === AUTH_MORE_DATA;
2178
+ function encodeErr(code, sqlState, message) {
2179
+ const state2 = sqlState.length === 5 ? sqlState : "HY000";
2180
+ return Buffer.concat([Buffer.from([255]), u16(code), Buffer.from(`#${state2}`, "latin1"), Buffer.from(message, "utf8")]);
2181
+ }
2182
+ var MysqlServerError = class extends Error {
2183
+ constructor(code, sqlState, message) {
2184
+ super(message);
2185
+ this.code = code;
2186
+ this.sqlState = sqlState;
2187
+ this.name = "MysqlServerError";
2188
+ }
2189
+ code;
2190
+ sqlState;
2191
+ };
2192
+ function serverErrorFromPacket(payload) {
2193
+ if (payload.length < 3 || payload[0] !== 255) return new MysqlServerError(0, "HY000", "unknown error");
2194
+ const code = payload.readUInt16LE(1);
2195
+ let rest = payload.subarray(3);
2196
+ let sqlState = "HY000";
2197
+ if (rest.length >= 6 && rest[0] === 35) {
2198
+ sqlState = rest.subarray(1, 6).toString("latin1");
2199
+ rest = rest.subarray(6);
2200
+ }
2201
+ return new MysqlServerError(code, sqlState, rest.toString("utf8"));
2202
+ }
2203
+ function newScramble() {
2204
+ const b = randomBytes(SCRAMBLE_LENGTH);
2205
+ for (let i = 0; i < b.length; i++) {
2206
+ let v = (b[i] ?? 0) & 127 | 1;
2207
+ if (v === 36) v = 37;
2208
+ b[i] = v;
2209
+ }
2210
+ return b;
2211
+ }
2212
+ function xor(a, b) {
2213
+ const out = Buffer.alloc(a.length);
2214
+ for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
2215
+ return out;
2216
+ }
2217
+ function scrambleNative(password, nonce) {
2218
+ if (password === "") return Buffer.alloc(0);
2219
+ const h1 = createHash("sha1").update(password, "utf8").digest();
2220
+ const h2 = createHash("sha1").update(h1).digest();
2221
+ const h3 = createHash("sha1").update(nonce).update(h2).digest();
2222
+ return xor(h1, h3);
2223
+ }
2224
+ function scrambleCachingSha2(password, nonce) {
2225
+ if (password === "") return Buffer.alloc(0);
2226
+ const h1 = createHash("sha256").update(password, "utf8").digest();
2227
+ const h2 = createHash("sha256").update(h1).digest();
2228
+ const h3 = createHash("sha256").update(h2).update(nonce).digest();
2229
+ return xor(h1, h3);
2230
+ }
2231
+ function scrambleFor(plugin, password, nonce) {
2232
+ switch (plugin) {
2233
+ case PLUGIN_NATIVE:
2234
+ return scrambleNative(password, nonce);
2235
+ case PLUGIN_CACHING_SHA2:
2236
+ return scrambleCachingSha2(password, nonce);
2237
+ default:
2238
+ throw new Error(`unsupported auth plugin "${plugin}"`);
2239
+ }
2240
+ }
2241
+ function scrambleMatches(received, expected) {
2242
+ return received.length === expected.length && timingSafeEqual(received, expected);
2243
+ }
2244
+
2245
+ // src/lib/gateway/forwarder.ts
2246
+ var DEFAULT_HOST = "127.0.0.1";
2247
+ var DEFAULT_HANDSHAKE_TIMEOUT_MS = 3e4;
2248
+ var REFRESH_RETRY_BACKOFFS_MS = [3e4, 6e4, 12e4, 3e5];
2249
+ var SERVER_VERSION = "8.0.40-meridian-forwarder";
2250
+ var CLOSE_GRACE_MS = 2e3;
2251
+ var ER_ACCESS_DENIED = 1045;
2252
+ var ER_HANDSHAKE = 1043;
2253
+ var CR_CONN_HOST_ERROR = 2003;
2254
+ var LocalRefusal = class extends Error {
2255
+ constructor(code, sqlState, message) {
2256
+ super(message);
2257
+ this.code = code;
2258
+ this.sqlState = sqlState;
2259
+ }
2260
+ code;
2261
+ sqlState;
2262
+ };
2263
+ function accessDenied(username) {
2264
+ return new LocalRefusal(ER_ACCESS_DENIED, "28000", `Access denied for user '${username}'@'localhost' (using password: YES)`);
2265
+ }
2266
+ function upstreamCapabilities(clientCapabilities, database) {
2267
+ let caps = clientCapabilities & LOCAL_CAPABILITIES | CLIENT.SSL | CLIENT.PROTOCOL_41 | CLIENT.SECURE_CONNECTION | CLIENT.PLUGIN_AUTH | CLIENT.PLUGIN_AUTH_LENENC_CLIENT_DATA;
2268
+ if (database !== "") caps |= CLIENT.CONNECT_WITH_DB;
2269
+ return caps >>> 0;
2270
+ }
2271
+ function waitFor(socket, event) {
2272
+ return new Promise((resolve2, reject) => {
2273
+ const onError = (error2) => {
2274
+ socket.off(event, onDone);
2275
+ reject(error2);
2276
+ };
2277
+ const onDone = () => {
2278
+ socket.off("error", onError);
2279
+ resolve2();
2280
+ };
2281
+ socket.once(event, onDone);
2282
+ socket.once("error", onError);
2283
+ });
2284
+ }
2285
+ function splice(a, b) {
2286
+ const closeOther = (other) => () => {
2287
+ if (other.destroyed) return;
2288
+ other.end();
2289
+ setTimeout(() => other.destroy(), CLOSE_GRACE_MS).unref();
2290
+ };
2291
+ a.on("close", closeOther(b));
2292
+ b.on("close", closeOther(a));
2293
+ a.pipe(b);
2294
+ b.pipe(a);
2295
+ }
2296
+ async function startGatewayForwarder(options) {
2297
+ const host = options.host ?? DEFAULT_HOST;
2298
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
2299
+ const refreshBeforeMs = options.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
2300
+ const now = options.now ?? Date.now;
2301
+ const emit = (event) => {
2302
+ try {
2303
+ options.onEvent?.(event);
2304
+ } catch {
2305
+ }
2306
+ };
2307
+ let certificate = options.certificate;
2308
+ let running = false;
2309
+ let stopped = false;
2310
+ let connectionId = 0;
2311
+ let refreshTimer;
2312
+ const sockets = /* @__PURE__ */ new Set();
2313
+ const track = (socket) => {
2314
+ sockets.add(socket);
2315
+ socket.once("close", () => sockets.delete(socket));
2316
+ socket.on("error", () => socket.destroy());
2317
+ };
2318
+ async function openUpstream(cert, client) {
2319
+ const raw = netConnect({ host: cert.host, port: cert.port });
2320
+ track(raw);
2321
+ raw.setNoDelay(true);
2322
+ await waitFor(raw, "connect");
2323
+ const rawReader = new PacketReader(raw);
2324
+ const greetingPacket = await rawReader.read();
2325
+ const greeting = parseGreeting(greetingPacket.payload);
2326
+ if ((greeting.capabilities & CLIENT.SSL) === 0) throw new Error("the gateway did not offer TLS");
2327
+ const capabilities = upstreamCapabilities(client.capabilities, client.database);
2328
+ let seq = greetingPacket.seq + 1;
2329
+ raw.write(encodePacket(seq, encodeSslRequest(capabilities, client.maxPacketSize, client.charset)));
2330
+ rawReader.release({ pause: false });
2331
+ const socket = tlsConnect({
2332
+ socket: raw,
2333
+ secureContext: cert.secureContext,
2334
+ host: cert.host,
2335
+ // SNI carries host names only; an IP is still verified against the certificate.
2336
+ ...isIP(cert.host) === 0 ? { servername: cert.host } : {},
2337
+ rejectUnauthorized: true
2338
+ });
2339
+ track(socket);
2340
+ socket.setNoDelay(true);
2341
+ await waitFor(socket, "secureConnect");
2342
+ const reader = new PacketReader(socket);
2343
+ let plugin = PLUGIN_CACHING_SHA2;
2344
+ seq++;
2345
+ socket.write(
2346
+ encodePacket(
2347
+ seq,
2348
+ encodeHandshakeResponse({
2349
+ capabilities,
2350
+ maxPacketSize: client.maxPacketSize,
2351
+ charset: client.charset,
2352
+ username: options.username,
2353
+ authResponse: scrambleCachingSha2(options.password, greeting.scramble),
2354
+ database: client.database,
2355
+ authPlugin: plugin,
2356
+ attributes: client.attributes
2357
+ })
2358
+ )
2359
+ );
2360
+ for (let round = 0; round < 6; round++) {
2361
+ const packet = await reader.read();
2362
+ seq = packet.seq;
2363
+ const payload = packet.payload;
2364
+ if (isOk(payload)) return { socket, ok: payload, leftover: reader.release({ pause: true }) };
2365
+ if (isErr(payload)) throw serverErrorFromPacket(payload);
2366
+ if (isAuthSwitch(payload)) {
2367
+ const request = parseAuthSwitchRequest(payload);
2368
+ plugin = request.plugin;
2369
+ seq++;
2370
+ socket.write(encodePacket(seq, scrambleFor(plugin, options.password, request.scramble)));
2371
+ continue;
2372
+ }
2373
+ if (isAuthMoreData(payload) && plugin === PLUGIN_CACHING_SHA2 && payload.length >= 2) {
2374
+ if (payload[1] === FAST_AUTH_SUCCESS) continue;
2375
+ if (payload[1] === PERFORM_FULL_AUTH) {
2376
+ seq++;
2377
+ socket.write(encodePacket(seq, Buffer.concat([Buffer.from(options.password, "utf8"), Buffer.from([0])])));
2378
+ continue;
2379
+ }
2380
+ }
2381
+ throw new Error(`unexpected packet 0x${(payload[0] ?? 0).toString(16)} from the gateway during authentication`);
2382
+ }
2383
+ throw new Error("the gateway's authentication did not finish");
2384
+ }
2385
+ async function authenticateLocal(local, reader) {
2386
+ const scramble = newScramble();
2387
+ connectionId = connectionId + 1 >>> 0;
2388
+ local.write(encodePacket(0, encodeGreeting(localGreeting(connectionId, scramble, SERVER_VERSION))));
2389
+ const first = await reader.read();
2390
+ let seq = first.seq;
2391
+ const client = parseHandshakeResponse(first.payload);
2392
+ if (first.payload.length === 32) {
2393
+ 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).");
2394
+ }
2395
+ if ((client.capabilities & (CLIENT.COMPRESS | CLIENT.ZSTD_COMPRESSION)) !== 0) {
2396
+ throw new LocalRefusal(ER_HANDSHAKE, "08S01", "Compression is not supported by the Meridian database gateway; turn it off in your client.");
2397
+ }
2398
+ if (client.username !== options.username) throw accessDenied(client.username);
2399
+ let authResponse = client.authResponse;
2400
+ let plugin = client.authPlugin;
2401
+ if (plugin === "" && (client.capabilities & CLIENT.SECURE_CONNECTION) !== 0) plugin = PLUGIN_NATIVE;
2402
+ if (plugin !== PLUGIN_NATIVE && plugin !== PLUGIN_CACHING_SHA2) {
2403
+ seq++;
2404
+ local.write(encodePacket(seq, encodeAuthSwitchRequest(PLUGIN_NATIVE, scramble)));
2405
+ const answer = await reader.read();
2406
+ seq = answer.seq;
2407
+ authResponse = answer.payload;
2408
+ plugin = PLUGIN_NATIVE;
2409
+ }
2410
+ const expected = plugin === PLUGIN_CACHING_SHA2 ? scrambleCachingSha2(options.password, scramble) : scrambleNative(options.password, scramble);
2411
+ if (!scrambleMatches(authResponse, expected)) throw accessDenied(client.username);
2412
+ if (plugin === PLUGIN_CACHING_SHA2) {
2413
+ seq++;
2414
+ local.write(encodePacket(seq, Buffer.from([AUTH_MORE_DATA, FAST_AUTH_SUCCESS])));
2415
+ }
2416
+ return { client, seq };
2417
+ }
2418
+ async function handleConnection(local) {
2419
+ track(local);
2420
+ local.setNoDelay(true);
2421
+ const reader = new PacketReader(local);
2422
+ let upstream;
2423
+ const timer = setTimeout(() => {
2424
+ emit({ type: "connection-error", message: "A local MySQL handshake timed out." });
2425
+ local.destroy();
2426
+ upstream?.destroy();
2427
+ }, handshakeTimeoutMs);
2428
+ timer.unref();
2429
+ let seq = 0;
2430
+ try {
2431
+ const authenticated = await authenticateLocal(local, reader);
2432
+ seq = authenticated.seq;
2433
+ const cert = certificate;
2434
+ let session;
2435
+ try {
2436
+ session = await openUpstream(cert, authenticated.client);
2437
+ } catch (error2) {
2438
+ if (error2 instanceof MysqlServerError) {
2439
+ throw new LocalRefusal(error2.code, error2.sqlState, error2.message);
2440
+ }
2441
+ throw new LocalRefusal(
2442
+ CR_CONN_HOST_ERROR,
2443
+ "HY000",
2444
+ `Cannot reach the Meridian database gateway at ${cert.host}:${cert.port}: ${describeError(error2)}`
2445
+ );
2446
+ }
2447
+ upstream = session.socket;
2448
+ clearTimeout(timer);
2449
+ local.write(encodePacket(seq + 1, session.ok));
2450
+ const pipelined = reader.release({ pause: true });
2451
+ if (pipelined.length > 0) upstream.write(pipelined);
2452
+ if (session.leftover.length > 0) local.write(session.leftover);
2453
+ splice(local, upstream);
2454
+ } catch (error2) {
2455
+ clearTimeout(timer);
2456
+ upstream?.destroy();
2457
+ if (error2 instanceof LocalRefusal) {
2458
+ if (error2.code !== ER_ACCESS_DENIED) emit({ type: "connection-error", message: error2.message });
2459
+ if (!local.destroyed) local.end(encodePacket(seq + 1, encodeErr(error2.code, error2.sqlState, error2.message)));
2460
+ return;
2461
+ }
2462
+ emit({ type: "connection-error", message: describeError(error2) });
2463
+ local.destroy();
2464
+ }
2465
+ }
2466
+ async function refresh() {
2467
+ if (options.refreshCertificate === void 0) return;
2468
+ const next = await options.refreshCertificate();
2469
+ certificate = next;
2470
+ emit({ type: "certificate-refreshed", expiresAt: next.expiresAt });
2471
+ }
2472
+ function scheduleRefresh(delayMs, attempt) {
2473
+ if (options.refreshCertificate === void 0 || stopped) return;
2474
+ refreshTimer = setTimeout(() => {
2475
+ refresh().then(
2476
+ () => scheduleRefresh(certificateRefreshDelay(certificate.expiresAt, now(), refreshBeforeMs), 0),
2477
+ (error2) => {
2478
+ const backoff = REFRESH_RETRY_BACKOFFS_MS[Math.min(attempt, REFRESH_RETRY_BACKOFFS_MS.length - 1)] ?? 3e5;
2479
+ emit({
2480
+ type: "error",
2481
+ message: `Could not renew the database gateway certificate (${describeError(error2)}); retrying in ${Math.round(backoff / 1e3)}s.`
2482
+ });
2483
+ scheduleRefresh(backoff, attempt + 1);
2484
+ }
2485
+ );
2486
+ }, delayMs);
2487
+ refreshTimer.unref();
2488
+ }
2489
+ const server = createServer((local) => {
2490
+ void handleConnection(local);
2491
+ });
2492
+ await new Promise((resolve2, reject) => {
2493
+ server.once("error", (error2) => {
2494
+ reject(
2495
+ error2.code === "EADDRINUSE" ? new CliError(`Port ${options.port} is already in use.`, {
2496
+ hint: "Pick a different port with --port, or free up that port and try again."
2497
+ }) : error2
2498
+ );
2499
+ });
2500
+ server.listen({ port: options.port, host, exclusive: true }, () => resolve2());
2501
+ });
2502
+ server.on("error", (error2) => emit({ type: "error", message: describeError(error2) }));
2503
+ const address = server.address();
2504
+ const port = typeof address === "object" && address !== null ? address.port : options.port;
2505
+ running = true;
2506
+ emit({ type: "ready", port });
2507
+ scheduleRefresh(certificateRefreshDelay(certificate.expiresAt, now(), refreshBeforeMs), 0);
2508
+ return {
2509
+ host,
2510
+ port,
2511
+ get running() {
2512
+ return running;
2513
+ },
2514
+ get certificate() {
2515
+ return certificate;
2516
+ },
2517
+ refresh,
2518
+ async stop() {
2519
+ if (stopped) return;
2520
+ stopped = true;
2521
+ running = false;
2522
+ if (refreshTimer !== void 0) clearTimeout(refreshTimer);
2523
+ const closed = new Promise((resolve2) => server.close(() => resolve2()));
2524
+ for (const socket of sockets) socket.destroy();
2525
+ await closed;
2526
+ }
2527
+ };
2528
+ }
2529
+
1745
2530
  // src/lib/port.ts
1746
- import { createServer } from "net";
2531
+ import { createServer as createServer2 } from "net";
1747
2532
  var DEFAULT_START_PORT = 3307;
1748
2533
  var DEFAULT_ATTEMPTS = 20;
1749
2534
  var MAX_PORT = 65535;
1750
2535
  function isPortFree(port, host = "127.0.0.1") {
1751
2536
  return new Promise((resolve2) => {
1752
- const server = createServer();
2537
+ const server = createServer2();
1753
2538
  server.once("error", () => {
1754
2539
  resolve2(false);
1755
2540
  });
@@ -1761,7 +2546,7 @@ function isPortFree(port, host = "127.0.0.1") {
1761
2546
  }
1762
2547
  function bindEphemeralPort(host) {
1763
2548
  return new Promise((resolve2) => {
1764
- const server = createServer();
2549
+ const server = createServer2();
1765
2550
  server.once("error", () => resolve2(null));
1766
2551
  server.listen({ port: 0, host, exclusive: true }, () => {
1767
2552
  const address = server.address();
@@ -1998,7 +2783,7 @@ function diagnoseMigrationFailure(root) {
1998
2783
  }
1999
2784
 
2000
2785
  // src/lib/proxy/binary.ts
2001
- import { createHash } from "crypto";
2786
+ import { createHash as createHash2 } from "crypto";
2002
2787
  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
2788
  import { join as join4 } from "path";
2004
2789
  var PROXY_VERSION = "2.24.1";
@@ -2047,7 +2832,7 @@ function resolveProxyAsset(options) {
2047
2832
  };
2048
2833
  }
2049
2834
  function sha256(data) {
2050
- return createHash("sha256").update(data).digest("hex");
2835
+ return createHash2("sha256").update(data).digest("hex");
2051
2836
  }
2052
2837
  var CHECKSUM_ROW_PATTERN = /\[([^\]]+)]\([^)]*\)\s*\|\s*([0-9a-fA-F]{64})/g;
2053
2838
  function parseChecksumTable(releaseBody) {
@@ -2188,8 +2973,8 @@ var PROBE_INTERVAL_MS = 150;
2188
2973
  var KILL_GRACE_MS = 2e3;
2189
2974
  var CRASH_BACKOFFS_MS = [200, 1e3, 3e3];
2190
2975
  var MAX_CRASH_RESTARTS = CRASH_BACKOFFS_MS.length;
2191
- var REFRESH_RETRY_BACKOFFS_MS = [3e4, 6e4, 12e4];
2192
- var MIN_REFRESH_DELAY_MS = 6e4;
2976
+ var REFRESH_RETRY_BACKOFFS_MS2 = [3e4, 6e4, 12e4];
2977
+ var MIN_REFRESH_DELAY_MS2 = 6e4;
2193
2978
  var LOG_RING_SIZE = 20;
2194
2979
  var STDERR_TAIL_LINES = 10;
2195
2980
  function buildProxyArgs(input2) {
@@ -2218,7 +3003,7 @@ function computeRefreshDelayMs(token, refreshSkewMs, assumedLifetimeMs) {
2218
3003
  if (!Number.isNaN(parsed)) expiresAtMs = parsed;
2219
3004
  }
2220
3005
  const effectiveExpiryMs = expiresAtMs ?? Date.now() + assumedLifetimeMs;
2221
- return Math.max(effectiveExpiryMs - Date.now() - refreshSkewMs, MIN_REFRESH_DELAY_MS);
3006
+ return Math.max(effectiveExpiryMs - Date.now() - refreshSkewMs, MIN_REFRESH_DELAY_MS2);
2222
3007
  }
2223
3008
  function buildStartupFailureError(code, signal, lastLines) {
2224
3009
  const tail = lastLines.slice(-STDERR_TAIL_LINES).join("\n");
@@ -2396,7 +3181,7 @@ async function startProxy(options) {
2396
3181
  }
2397
3182
  function scheduleRefreshRetry() {
2398
3183
  if (stopped) return;
2399
- const delayMs = REFRESH_RETRY_BACKOFFS_MS[Math.min(refreshBackoffIndex, REFRESH_RETRY_BACKOFFS_MS.length - 1)] ?? 12e4;
3184
+ const delayMs = REFRESH_RETRY_BACKOFFS_MS2[Math.min(refreshBackoffIndex, REFRESH_RETRY_BACKOFFS_MS2.length - 1)] ?? 12e4;
2400
3185
  refreshBackoffIndex += 1;
2401
3186
  if (refreshTimer !== void 0) clearTimeout(refreshTimer);
2402
3187
  refreshTimer = setTimeoutImpl(() => {
@@ -2596,6 +3381,8 @@ function resolveDeps(overrides) {
2596
3381
  return {
2597
3382
  ensureProxyBinary,
2598
3383
  startProxy,
3384
+ issueGatewayCertificate,
3385
+ startGatewayForwarder,
2599
3386
  findFreePort,
2600
3387
  patchEnvFile,
2601
3388
  waitForDatabase,
@@ -2722,6 +3509,97 @@ function handleProxyEvent(event) {
2722
3509
  return;
2723
3510
  }
2724
3511
  }
3512
+ async function pickLocalPort(options, deps) {
3513
+ const findPortOptions = { start: DEFAULT_PORT_START };
3514
+ if (options.port !== void 0) findPortOptions.preferred = options.port;
3515
+ const port = await deps.findFreePort(findPortOptions);
3516
+ if (options.port !== void 0 && port !== options.port) {
3517
+ throw new CliError(`Port ${options.port} is already in use.`, {
3518
+ hint: "Pick a different port with --port, or free up that port and try again."
3519
+ });
3520
+ }
3521
+ return port;
3522
+ }
3523
+ async function startProxyEndpoint(client, appId, devDatabase, credentials, options, deps) {
3524
+ const proxyToken = await withSpinner(
3525
+ "Requesting a Cloud SQL Auth Proxy token...",
3526
+ () => client.issueProxyToken(appId),
3527
+ { success: "Issued a Cloud SQL Auth Proxy token." }
3528
+ );
3529
+ const binaryPath = await withSpinner(
3530
+ "Preparing the Cloud SQL Auth Proxy...",
3531
+ (spinner2) => {
3532
+ const ensureOptions = {
3533
+ onProgress: (received, total) => {
3534
+ spinner2.update(
3535
+ total !== void 0 ? `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)}/${formatMb(total)} MB)` : `Downloading the Cloud SQL Auth Proxy... (${formatMb(received)} MB)`
3536
+ );
3537
+ }
3538
+ };
3539
+ return deps.ensureProxyBinary(ensureOptions);
3540
+ },
3541
+ { success: "Cloud SQL Auth Proxy is ready." }
3542
+ );
3543
+ const port = await pickLocalPort(options, deps);
3544
+ const instanceConnectionName = firstNonEmpty(
3545
+ credentials.instance_connection_name,
3546
+ proxyToken.instance_connection_name,
3547
+ devDatabase.instance_connection_name
3548
+ );
3549
+ if (instanceConnectionName === void 0) {
3550
+ throw new CliError("The Meridian API did not return a Cloud SQL instance connection name.", {
3551
+ hint: "Try again, or contact Meridian support if this persists."
3552
+ });
3553
+ }
3554
+ return withSpinner(
3555
+ `Starting the Cloud SQL Auth Proxy on 127.0.0.1:${port}...`,
3556
+ () => deps.startProxy({
3557
+ binaryPath,
3558
+ instanceConnectionName,
3559
+ token: toProxyToken(proxyToken),
3560
+ port,
3561
+ ...options.refreshToken !== false ? { refreshToken: async () => toProxyToken(await client.issueProxyToken(appId)) } : {},
3562
+ onEvent: handleProxyEvent
3563
+ }),
3564
+ { success: `Cloud SQL Auth Proxy listening on 127.0.0.1:${port}.` }
3565
+ );
3566
+ }
3567
+ function handleForwarderEvent(event) {
3568
+ switch (event.type) {
3569
+ case "ready":
3570
+ debug(`Database gateway forwarder listening on port ${event.port}.`);
3571
+ return;
3572
+ case "connection-error":
3573
+ debug(`Database gateway connection failed: ${event.message}`);
3574
+ return;
3575
+ case "certificate-refreshed":
3576
+ step(`Renewed the database gateway certificate (expires ${event.expiresAt}).`);
3577
+ return;
3578
+ case "error":
3579
+ warn(`Database gateway: ${event.message} Restart \`meridian dev\` if the database becomes unreachable.`);
3580
+ return;
3581
+ }
3582
+ }
3583
+ async function startGatewayEndpoint(client, appId, credentials, options, deps) {
3584
+ const certificate = await withSpinner(
3585
+ "Requesting a database gateway certificate...",
3586
+ () => deps.issueGatewayCertificate(client, appId),
3587
+ { success: (cert) => `Issued a database gateway certificate (expires ${cert.expiresAt}).` }
3588
+ );
3589
+ const port = await pickLocalPort(options, deps);
3590
+ return withSpinner(
3591
+ `Starting the database gateway forwarder on 127.0.0.1:${port}...`,
3592
+ () => deps.startGatewayForwarder({
3593
+ port,
3594
+ username: credentials.username,
3595
+ password: credentials.password,
3596
+ certificate,
3597
+ ...options.refreshToken !== false ? { refreshCertificate: () => deps.issueGatewayCertificate(client, appId) } : {},
3598
+ onEvent: handleForwarderEvent
3599
+ }),
3600
+ { success: `Database gateway forwarder listening on 127.0.0.1:${port} (${certificate.host}).` }
3601
+ );
3602
+ }
2725
3603
  async function startDevSession(options = {}) {
2726
3604
  const deps = resolveDeps(options.deps);
2727
3605
  let proxy;
@@ -2738,55 +3616,8 @@ async function startDevSession(options = {}) {
2738
3616
  success: (creds) => `Fetched credentials for database "${creds.database}"${typeof creds.expires_at === "string" ? ` (expire ${creds.expires_at})` : ""}.`
2739
3617
  }
2740
3618
  );
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
- );
3619
+ const access = devDatabase.access === "gateway" ? "gateway" : "proxy";
3620
+ proxy = access === "gateway" ? await startGatewayEndpoint(client, appId, credentials, options, deps) : await startProxyEndpoint(client, appId, devDatabase, credentials, options, deps);
2790
3621
  const connection = connectionInfoFromCredentials(credentials, { host: proxy.host, port: proxy.port });
2791
3622
  if (options.waitForDb !== false) {
2792
3623
  await withSpinner(
@@ -2838,6 +3669,7 @@ async function startDevSession(options = {}) {
2838
3669
  connection,
2839
3670
  databaseUrl,
2840
3671
  ...shadowDatabaseUrl !== void 0 ? { shadowDatabaseUrl } : {},
3672
+ access,
2841
3673
  proxy,
2842
3674
  env: {
2843
3675
  ...process.env,
@@ -3506,7 +4338,7 @@ function printDevSummary(session, noEnv) {
3506
4338
  const entries = [
3507
4339
  ["App", appLabel(session.project)],
3508
4340
  ["Database", session.devDatabase.database],
3509
- ["Proxy", `127.0.0.1:${session.proxy.port}`],
4341
+ [session.access === "gateway" ? "Gateway" : "Proxy", `127.0.0.1:${session.proxy.port}`],
3510
4342
  ["Env file", envEntry],
3511
4343
  ["DATABASE_URL", redactDatabaseUrl(session.databaseUrl)]
3512
4344
  ];
@@ -3521,6 +4353,7 @@ function buildDevJsonPayload(session) {
3521
4353
  return {
3522
4354
  app: appLabel(session.project),
3523
4355
  database: session.devDatabase.database,
4356
+ access: session.access,
3524
4357
  proxy: { host: session.proxy.host, port: session.proxy.port },
3525
4358
  database_url: redactDatabaseUrl(session.databaseUrl),
3526
4359
  env_file: session.envPatch?.path ?? null,