neon 4.13.0 → 4.14.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.
Files changed (41) hide show
  1. package/README.md +55 -13
  2. package/dist/_chunks/{credential_io-YeAxg9Xn.js → credential_io-D8AR-jvB.js} +4 -4
  3. package/dist/_chunks/{env-NbA61JR3.js → env-CZmstYrY.js} +71 -59
  4. package/dist/_chunks/{env_services-Tz9G4JeT.js → env_services-CAWrZTWa.js} +138 -41
  5. package/dist/_shared/auth_selection.js +83 -0
  6. package/dist/_shared/credentials.js +188 -0
  7. package/dist/_shared/paths.js +151 -0
  8. package/dist/_shared/profiles.js +226 -0
  9. package/dist/_shared/secure_file.js +41 -0
  10. package/dist/analytics.js +2 -2
  11. package/dist/auth.js +101 -11
  12. package/dist/auth_context.js +2 -11
  13. package/dist/claimable/api.js +2 -1
  14. package/dist/commands/ask.js +2 -1
  15. package/dist/commands/auth.js +155 -63
  16. package/dist/commands/checkout.js +28 -9
  17. package/dist/commands/claim.js +11 -7
  18. package/dist/commands/config.js +9 -9
  19. package/dist/commands/deploy.js +2 -1
  20. package/dist/commands/deploy_help.js +10 -0
  21. package/dist/commands/dev.js +147 -36
  22. package/dist/commands/env.js +33 -44
  23. package/dist/commands/functions.js +63 -2
  24. package/dist/commands/profile.js +2 -2
  25. package/dist/config_services.js +36 -1
  26. package/dist/credential_io.js +1 -1
  27. package/dist/custom_domains_api.js +37 -0
  28. package/dist/dev/env.js +1 -1
  29. package/dist/dev/functions.js +28 -11
  30. package/dist/dev/websocket.js +1 -1
  31. package/dist/env_services.js +2 -2
  32. package/dist/index.js +73 -20
  33. package/dist/init/build_config.js +4 -0
  34. package/dist/neon_services.js +2 -2
  35. package/dist/psql/wire/connection.js +1 -1
  36. package/dist/refresh_lock.js +44 -0
  37. package/dist/retire_credential.js +1 -1
  38. package/dist/test_utils/neon_api_server.js +60 -0
  39. package/dist/test_utils/rotating_oauth_server.js +94 -0
  40. package/dist/utils/config_diff.js +2 -13
  41. package/package.json +7 -5
@@ -21,21 +21,38 @@ const resolveFunctionsFromConfig = async (cwd, branchName) => {
21
21
  name: branchName ?? "local",
22
22
  exists: branchName !== void 0
23
23
  }).preview?.functions ?? []).map((fn) => {
24
- const source = isAbsolute(fn.source) ? fn.source : resolve(configDir, fn.source);
24
+ const source = resolveFunctionSource(fn.source, configDir);
25
25
  if (!existsSync(source)) throw new Error(`Function "${fn.slug}" points at a source that does not exist: ${source} (from neon.ts "${fn.source}"). Fix the source path and re-run.`);
26
- return {
27
- slug: fn.slug,
28
- name: fn.name,
29
- source,
30
- ...devPort(fn.dev) !== void 0 ? { port: devPort(fn.dev) } : {},
31
- env: { ...fn.env },
32
- ...fn.externalPackages ? { externalPackages: fn.externalPackages.map((pkg) => pkg.name) } : {},
33
- ...fn.bundler !== "esbuild" ? { bundler: fn.bundler } : {}
34
- };
26
+ return toPlannedFunction(fn, source);
35
27
  })
36
28
  };
37
29
  };
38
30
  /**
31
+ * The neon.ts function whose `source` is this file, if any. Sibling sources are not
32
+ * required to exist: `neon dev --source` is serving one file, not the whole set.
33
+ */
34
+ const findConfigFunctionBySource = async (cwd, source) => {
35
+ const loaded = await loadNeonConfig(cwd);
36
+ if (!loaded) return void 0;
37
+ return (resolveConfig(loaded.config, {
38
+ name: "local",
39
+ exists: false
40
+ }).preview?.functions ?? []).map((fn) => toPlannedFunction(fn, resolveFunctionSource(fn.source, loaded.configDir))).find((fn) => fn.source === source);
41
+ };
42
+ const resolveFunctionSource = (source, configDir) => isAbsolute(source) ? source : resolve(configDir, source);
43
+ const toPlannedFunction = (fn, source) => {
44
+ const port = devPort(fn.dev);
45
+ return {
46
+ slug: fn.slug,
47
+ name: fn.name,
48
+ source,
49
+ ...port !== void 0 ? { port } : {},
50
+ env: { ...fn.env },
51
+ ...fn.externalPackages ? { externalPackages: fn.externalPackages.map((pkg) => pkg.name) } : {},
52
+ ...fn.bundler !== "esbuild" ? { bundler: fn.bundler } : {}
53
+ };
54
+ };
55
+ /**
39
56
  * Read the `port` off a {@link FunctionDevConfig}. `undefined` when no `dev.port` is set
40
57
  * (the supervisor then searches for a free port).
41
58
  */
@@ -61,4 +78,4 @@ const loadNeonConfig = async (cwd) => {
61
78
  }
62
79
  };
63
80
  //#endregion
64
- export { resolveFunctionsFromConfig };
81
+ export { findConfigFunctionBySource, resolveFunctionsFromConfig };
@@ -357,7 +357,7 @@ const handleUpgrade = async (req, socket, head, { fetch: fetchHandler, upgrade,
357
357
  return;
358
358
  }
359
359
  if (record.claimed) {
360
- log("websocket_upgrade_response_lost: upgradeWebSocket() was called for this request but the response returned from the handler is not the one it produced. `Response.clone()` and rebuilding a response (`new Response(res.body, res)`, as response-rewriting middleware does) both discard the upgrade. Return the `response` from upgradeWebSocket() unchanged.");
360
+ log("websocket_upgrade_response_lost: upgradeWebSocket() was called for this request but the response returned from the handler is not the one it produced. `Response.clone()` and rebuilding a response (`new Response(res.body, res)`, as response-rewriting middleware does) both discard the upgrade. Return the `response` from upgradeWebSocket() unchanged.\nWith @neon/functions/hono the helper returns the response for you, so this is a middleware on the upgrade route rebuilding it: `cors()` always does, and so does any middleware that reads `c.res` before `await next()` or calls `c.header()` after it. Take it off this route. It usually surfaces first as `RangeError: init[\"status\"] must be in the range of 200 to 599` from inside Hono, which names none of this.");
361
361
  abortClaimedUpgrade(record);
362
362
  writeUpgradeRejection(socket, 500, "websocket_upgrade_response_lost");
363
363
  return;
@@ -1,2 +1,2 @@
1
- import { a as envKeysForSelection, c as ownedEnvServiceKeys, d as servicesForEnvKeys, i as ENV_PULL_UNAVAILABLE, l as parseEnvPullKeys, n as ENV_PULL_KEYS, o as envPullFlagValue, r as ENV_PULL_SERVICES, s as envServiceKeys, t as BRANCH_ENV_KEY, u as serviceForEnvKey } from "./_chunks/env_services-Tz9G4JeT.js";
2
- export { BRANCH_ENV_KEY, ENV_PULL_KEYS, ENV_PULL_SERVICES, ENV_PULL_UNAVAILABLE, envKeysForSelection, envPullFlagValue, envServiceKeys, ownedEnvServiceKeys, parseEnvPullKeys, serviceForEnvKey, servicesForEnvKeys };
1
+ import { a as envKeysForSelection, c as ownedEnvServiceKeys, d as servicesForEnvKeys, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, n as ENV_PULL_KEYS, o as envPullFlagValue, r as ENV_PULL_KEY_HELP, s as envServiceKeys, t as BRANCH_ENV_KEY, u as serviceForEnvKey } from "./_chunks/env_services-CAWrZTWa.js";
2
+ export { BRANCH_ENV_KEY, ENV_PULL_KEYS, ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, envServiceKeys, ownedEnvServiceKeys, parseEnvPullKeys, serviceForEnvKey, servicesForEnvKeys };
package/dist/index.js CHANGED
@@ -1,16 +1,17 @@
1
+ import { t as storeFor } from "./_chunks/credential_io-D8AR-jvB.js";
1
2
  import { log } from "./log.js";
2
3
  import pkg_default from "./pkg.js";
3
4
  import { isNeonApiError, messageFromBody } from "./api.js";
5
+ import { authFailureMessage, getAuthContext, locationFromContext } from "./auth_context.js";
4
6
  import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
5
7
  import { ensureConfigDir } from "./config.js";
6
- import { authFailureMessage, credentialsToClearOn401, getAuthContext } from "./auth_context.js";
7
8
  import { currentContextFile, enrichFromContext } from "./context.js";
8
9
  import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent } from "./analytics.js";
9
10
  import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
10
- import { defaultClientID } from "./auth.js";
11
+ import { AuthRefreshError, defaultClientID } from "./auth.js";
11
12
  import { getCliName } from "./utils/cli_name.js";
12
13
  import { showHelp } from "./help.js";
13
- import { deleteCredentialsAt, ensureAuth } from "./commands/auth.js";
14
+ import { ensureAuth, isAccessTokenUsable, refreshStoredCredentials } from "./commands/auth.js";
14
15
  import { rewriteUnknownAgentArg } from "./init/plan.js";
15
16
  import commands_default from "./commands/index.js";
16
17
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
@@ -123,7 +124,64 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
123
124
  }).alias("help", "h").middleware(async (args) => {
124
125
  if (args.help || args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0])) await showHelp(builder);
125
126
  }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(commands_default).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(false);
126
- async function handleError(msg, err) {
127
+ function supersededOnDisk(rejectedToken) {
128
+ if (rejectedToken === void 0) return false;
129
+ const context = getAuthContext();
130
+ if (context === null) return false;
131
+ const at = locationFromContext(context);
132
+ if (at === null) return false;
133
+ try {
134
+ const loaded = storeFor(context.configDir).read(at);
135
+ return loaded !== null && loaded.credentials.access_token !== rejectedToken && isAccessTokenUsable(loaded.credentials, Date.now());
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ async function recoverFrom401(canRetry) {
141
+ const context = getAuthContext();
142
+ if (context === null || context.source !== "stored-credentials") {
143
+ log.error(authFailureMessage(context));
144
+ return false;
145
+ }
146
+ if (context.refreshed === true) {
147
+ log.error(authFailureMessage(context));
148
+ return false;
149
+ }
150
+ if (!canRetry) {
151
+ log.error(authFailureMessage(context));
152
+ return false;
153
+ }
154
+ if (supersededOnDisk(context.accessToken)) {
155
+ log.debug("The rejected token has already been replaced on disk; retrying with the current one");
156
+ return true;
157
+ }
158
+ const at = locationFromContext(context);
159
+ if (at === null || context.oauthHost === void 0 || context.clientId === void 0) {
160
+ log.error(authFailureMessage(context));
161
+ return false;
162
+ }
163
+ try {
164
+ if (await refreshStoredCredentials(at, {
165
+ apiHost: "",
166
+ oauthHost: context.oauthHost,
167
+ clientId: context.clientId,
168
+ configDir: context.configDir
169
+ })) {
170
+ log.debug("Refreshed the stored session after a 401; retrying");
171
+ return true;
172
+ }
173
+ } catch (err) {
174
+ if (err instanceof AuthRefreshError) {
175
+ log.error(err.message);
176
+ if (err.terminal) log.error(`Run \`neon auth --profile ${context.profile ?? "DEFAULT"}\` to sign in again.`);
177
+ return false;
178
+ }
179
+ log.debug("Refresh after 401 failed: %s", err instanceof Error ? err.message : "unknown error");
180
+ }
181
+ log.error(authFailureMessage(context));
182
+ return false;
183
+ }
184
+ async function handleError(msg, err, canRetry) {
127
185
  if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
128
186
  await showHelp(builder);
129
187
  process.exit(0);
@@ -142,6 +200,15 @@ async function handleError(msg, err) {
142
200
  return false;
143
201
  }
144
202
  }
203
+ if (err instanceof AuthRefreshError) {
204
+ log.error(err.message);
205
+ if (err.terminal) {
206
+ const context = getAuthContext();
207
+ log.error(`Run \`neon auth --profile ${context?.profile ?? "DEFAULT"}\` to sign in again.`);
208
+ }
209
+ sendError(err, "AUTH_FAILED");
210
+ return false;
211
+ }
145
212
  if (isNetworkError(err)) {
146
213
  log.error(NETWORK_ERROR_MESSAGE);
147
214
  const error = err instanceof Error ? err : new Error(NETWORK_ERROR_MESSAGE);
@@ -155,21 +222,7 @@ async function handleError(msg, err) {
155
222
  return false;
156
223
  } else if (err.status === 401) {
157
224
  sendError(err, "AUTH_FAILED");
158
- const context = getAuthContext();
159
- const staleCredentials = credentialsToClearOn401(context);
160
- if (staleCredentials === null) {
161
- log.error(authFailureMessage(context));
162
- return false;
163
- }
164
- log.info("Authentication failed, deleting credentials...");
165
- try {
166
- if (context === null) return false;
167
- deleteCredentialsAt(staleCredentials, context.configDir);
168
- return true;
169
- } catch (deleteErr) {
170
- log.debug("Failed to delete credentials: %s", deleteErr instanceof Error ? deleteErr.message : "unknown error");
171
- return false;
172
- }
225
+ return await recoverFrom401(canRetry);
173
226
  } else {
174
227
  const serverMessage = messageFromBody(err.data);
175
228
  if (serverMessage) log.error(serverMessage);
@@ -206,7 +259,7 @@ async function handleError(msg, err) {
206
259
  break;
207
260
  } catch (err) {
208
261
  attempts++;
209
- if (!await handleError("", err) || attempts >= MAX_ATTEMPTS) {
262
+ if (!await handleError("", err, attempts < MAX_ATTEMPTS) || attempts >= MAX_ATTEMPTS) {
210
263
  await closeAnalytics();
211
264
  process.exit(1);
212
265
  }
@@ -0,0 +1,4 @@
1
+ //#region src/init/build_config.ts
2
+ const INTERNAL_VSX_GALLERY = "";
3
+ //#endregion
4
+ export { INTERNAL_VSX_GALLERY };
@@ -48,8 +48,8 @@ const deprecatedServiceMessage = (used, canonical) => `"${used}" is the old name
48
48
  *
49
49
  * An unrecognized name is rejected rather than dropped: a typo would otherwise act on
50
50
  * everything *except* the service that was asked for, and report success. A name that is a
51
- * real service but not one this command supports says so specifically — "functions has no env
52
- * variables" is a different problem from a typo, and has a different fix.
51
+ * real service but not one this command supports says so specifically — "postgres is on
52
+ * every branch" is a different problem from a typo, and has a different fix.
53
53
  */
54
54
  const parseServices = (raw, options) => {
55
55
  const { allowed, flag, noneMeans, whyUnavailable = {}, onDeprecated } = options;
@@ -4,9 +4,9 @@ import { PipelineSession } from "./pipeline.js";
4
4
  import { createScramClient } from "./sasl.js";
5
5
  import { negotiateTls } from "./tls.js";
6
6
  import { createHash } from "node:crypto";
7
+ import * as net from "node:net";
7
8
  import { Buffer } from "node:buffer";
8
9
  import * as dns from "node:dns/promises";
9
- import * as net from "node:net";
10
10
  import * as tls from "node:tls";
11
11
  //#region src/psql/wire/connection.ts
12
12
  /**
@@ -0,0 +1,44 @@
1
+ import { closeSync, openSync, statSync, unlinkSync } from "node:fs";
2
+ //#region src/refresh_lock.ts
3
+ const WAIT_MS = 2e4;
4
+ const RETRY_MS = 50;
5
+ const STALE_MS = 6e4;
6
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+ const isEexist = (err) => err instanceof Error && err.code === "EEXIST";
8
+ const isStale = (path) => {
9
+ try {
10
+ return Date.now() - statSync(path).mtimeMs > STALE_MS;
11
+ } catch {
12
+ return true;
13
+ }
14
+ };
15
+ /**
16
+ * Serialize exchanges because replaying a one-time refresh token can revoke the
17
+ * newly issued token family.
18
+ */
19
+ const withExclusiveLock = async (lockPath, fn) => {
20
+ const deadline = Date.now() + WAIT_MS;
21
+ for (;;) try {
22
+ const fd = openSync(lockPath, "wx", 384);
23
+ try {
24
+ return await fn();
25
+ } finally {
26
+ closeSync(fd);
27
+ try {
28
+ unlinkSync(lockPath);
29
+ } catch {}
30
+ }
31
+ } catch (err) {
32
+ if (!isEexist(err)) throw err;
33
+ if (isStale(lockPath)) {
34
+ try {
35
+ unlinkSync(lockPath);
36
+ } catch {}
37
+ continue;
38
+ }
39
+ if (Date.now() >= deadline) throw new Error(`Could not lock ${lockPath} to refresh the stored session. Retry the command.`);
40
+ await delay(RETRY_MS);
41
+ }
42
+ };
43
+ //#endregion
44
+ export { withExclusiveLock };
@@ -1,4 +1,4 @@
1
- import { a as OAUTH, d as scopeOf, i as API_KEY, l as interpretCredentials, t as storeFor, u as isSameCredential } from "./_chunks/credential_io-YeAxg9Xn.js";
1
+ import { a as OAUTH, d as scopeOf, i as API_KEY, l as interpretCredentials, t as storeFor, u as isSameCredential } from "./_chunks/credential_io-D8AR-jvB.js";
2
2
  import { log } from "./log.js";
3
3
  import { getApiClient } from "./api.js";
4
4
  import { revokeToken } from "./auth.js";
@@ -0,0 +1,60 @@
1
+ import { createServer } from "node:http";
2
+ //#region src/test_utils/neon_api_server.ts
3
+ const startNeonApiServer = async (user = {
4
+ id: "user-1",
5
+ email: "user@example.com"
6
+ }) => {
7
+ const rejected = /* @__PURE__ */ new Set();
8
+ const seen = [];
9
+ let rejectAll = false;
10
+ let failUserLookup = false;
11
+ const server = createServer((req, res) => {
12
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
13
+ const token = (req.headers.authorization ?? "").replace(/^Bearer\s+/i, "");
14
+ if (url.pathname !== "/users/me") return json(res, 404, { message: "Not Found" });
15
+ seen.push(token);
16
+ if (rejectAll || token === "" || rejected.has(token)) return json(res, 401, { message: "Unauthorized" });
17
+ if (failUserLookup) return json(res, 500, { message: "Internal Server Error" });
18
+ return json(res, 200, {
19
+ id: user.id,
20
+ email: user.email,
21
+ login: "tester",
22
+ name: "Tester",
23
+ projects_limit: 10,
24
+ branches_limit: 10,
25
+ max_autoscaling_limit: 1,
26
+ plan: "free",
27
+ auth_accounts: []
28
+ });
29
+ });
30
+ await new Promise((resolve) => {
31
+ server.listen(0, "127.0.0.1", resolve);
32
+ });
33
+ return {
34
+ url: `http://127.0.0.1:${server.address().port}`,
35
+ reject: (accessToken) => {
36
+ rejected.add(accessToken);
37
+ },
38
+ rejectAll: (value) => {
39
+ rejectAll = value;
40
+ },
41
+ failUserLookup: (value) => {
42
+ failUserLookup = value;
43
+ },
44
+ seenTokens: () => [...seen],
45
+ stop: () => closeServer(server)
46
+ };
47
+ };
48
+ const json = (res, status, body) => {
49
+ const payload = JSON.stringify(body);
50
+ res.writeHead(status, {
51
+ "content-type": "application/json",
52
+ "content-length": Buffer.byteLength(payload)
53
+ });
54
+ res.end(payload);
55
+ };
56
+ const closeServer = (server) => new Promise((resolve, reject) => {
57
+ server.close((err) => err ? reject(err) : resolve());
58
+ });
59
+ //#endregion
60
+ export { startNeonApiServer };
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ //#region src/test_utils/rotating_oauth_server.ts
4
+ const startRotatingOauthServer = async (options = {}) => {
5
+ const lifetime = options.accessTokenLifetimeSeconds ?? 3600;
6
+ const liveRefreshTokens = /* @__PURE__ */ new Set();
7
+ let refreshAttempts = 0;
8
+ let rotations = 0;
9
+ let unreachable = false;
10
+ const issue = ({ expiresIn = lifetime } = {}) => {
11
+ const refresh = `refresh-${randomUUID()}`;
12
+ liveRefreshTokens.add(refresh);
13
+ return {
14
+ access_token: `access-${randomUUID()}`,
15
+ refresh_token: refresh,
16
+ expires_in: expiresIn,
17
+ token_type: "bearer"
18
+ };
19
+ };
20
+ const server = createServer((req, res) => {
21
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
22
+ if (url.pathname === "/.well-known/openid-configuration") return json(res, 200, {
23
+ issuer: baseUrl(),
24
+ token_endpoint: `${baseUrl()}/oauth2/token`,
25
+ authorization_endpoint: `${baseUrl()}/oauth2/authorize`,
26
+ revocation_endpoint: `${baseUrl()}/oauth2/revoke`,
27
+ response_types_supported: ["code"],
28
+ grant_types_supported: ["authorization_code", "refresh_token"],
29
+ token_endpoint_auth_methods_supported: ["none"],
30
+ scopes_supported: [
31
+ "openid",
32
+ "offline",
33
+ "offline_access"
34
+ ]
35
+ });
36
+ if (url.pathname === "/oauth2/token" && req.method === "POST") return readBody(req, (body) => {
37
+ const form = new URLSearchParams(body);
38
+ if (form.get("grant_type") !== "refresh_token") return json(res, 400, { error: "unsupported_grant_type" });
39
+ refreshAttempts += 1;
40
+ if (unreachable) {
41
+ res.destroy();
42
+ return;
43
+ }
44
+ const presented = form.get("refresh_token") ?? "";
45
+ if (!liveRefreshTokens.delete(presented)) return json(res, 400, {
46
+ error: "invalid_grant",
47
+ error_description: "The provided authorization grant or refresh token is invalid, expired, or revoked."
48
+ });
49
+ rotations += 1;
50
+ return json(res, 200, {
51
+ ...issue(),
52
+ scope: "openid offline offline_access"
53
+ });
54
+ });
55
+ return json(res, 404, { error: "not_found" });
56
+ });
57
+ await new Promise((resolve) => {
58
+ server.listen(0, "127.0.0.1", resolve);
59
+ });
60
+ const baseUrl = () => `http://127.0.0.1:${server.address().port}`;
61
+ return {
62
+ url: baseUrl(),
63
+ refreshAttempts: () => refreshAttempts,
64
+ rotations: () => rotations,
65
+ issue,
66
+ revoke: (refreshToken) => {
67
+ liveRefreshTokens.delete(refreshToken);
68
+ },
69
+ setUnreachable: (value) => {
70
+ unreachable = value;
71
+ },
72
+ stop: () => closeServer(server)
73
+ };
74
+ };
75
+ const json = (res, status, body) => {
76
+ const payload = JSON.stringify(body);
77
+ res.writeHead(status, {
78
+ "content-type": "application/json;charset=UTF-8",
79
+ "content-length": Buffer.byteLength(payload)
80
+ });
81
+ res.end(payload);
82
+ };
83
+ const readBody = (req, next) => {
84
+ let body = "";
85
+ req.on("data", (chunk) => {
86
+ body += chunk.toString();
87
+ });
88
+ req.on("end", () => next(body));
89
+ };
90
+ const closeServer = (server) => new Promise((resolve, reject) => {
91
+ server.close((err) => err ? reject(err) : resolve());
92
+ });
93
+ //#endregion
94
+ export { startRotatingOauthServer };
@@ -104,18 +104,6 @@ const appliedBranchFields = (change) => {
104
104
  default: return expandField(field, void 0, details.settings);
105
105
  }
106
106
  };
107
- /**
108
- * Render the applied (or planned) changes as a `git diff`-style list:
109
- *
110
- * - **Service changes** (auth / Data API / buckets / functions) are additions
111
- * with no meaningful "before", so they list as green `+ <label>` lines
112
- * (`~ <label>` for the lone Data API settings *update*).
113
- * - **Branch setting changes** group per branch and render as sorted
114
- * `field → value` lines under a `~ <branch>` header.
115
- *
116
- * Returns "" when there is nothing to show. Pure — callers own the heading
117
- * spacing and any surrounding summary.
118
- */
119
107
  const renderAppliedChanges = (changes, title, opts) => {
120
108
  if (changes.length === 0) return "";
121
109
  const paint = palette(opts.color);
@@ -123,7 +111,8 @@ const renderAppliedChanges = (changes, title, opts) => {
123
111
  const services = changes.filter((c) => c.kind === "service").sort((a, b) => a.identifier.localeCompare(b.identifier));
124
112
  for (const service of services) {
125
113
  const label = serviceLabel(service.identifier);
126
- lines.push(service.action === "create" ? ` ${paint.added(`+ ${label}`)}` : ` ${paint.arrow(`~ ${label}`)}`);
114
+ const line = service.action === "create" ? `+ ${label}` : service.action === "delete" ? `- ${label}` : `~ ${label}`;
115
+ lines.push(` ${service.action === "create" ? paint.added(line) : service.action === "delete" ? paint.removed(line) : paint.arrow(line)}`);
127
116
  }
128
117
  const byBranch = /* @__PURE__ */ new Map();
129
118
  for (const change of changes.filter((c) => c.kind === "branch")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.13.0",
3
+ "version": "4.14.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.1.0",
67
- "@neon/sdk": "3.0.0",
68
- "@neon/config-runtime": "1.1.0"
66
+ "@neon/config": "1.2.0",
67
+ "@neon/sdk": "3.1.0",
68
+ "@neon/config-runtime": "1.2.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -73,6 +73,7 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@apidevtools/swagger-parser": "12.1.0",
76
+ "hono": "^4.12.25",
76
77
  "@rollup/plugin-commonjs": "25.0.8",
77
78
  "@rollup/plugin-json": "6.1.0",
78
79
  "@rollup/plugin-node-resolve": "15.2.3",
@@ -97,8 +98,9 @@
97
98
  "typescript": "^5.9.0",
98
99
  "vitest": "^3.0.9",
99
100
  "@neon-internals/cli-core": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.7",
100
102
  "@neon/e2e-harness": "0.0.0",
101
- "@neon-internals/env-core": "0.0.6"
103
+ "@neon/functions": "0.9.0"
102
104
  },
103
105
  "publishConfig": {
104
106
  "access": "public",