neonctl 2.28.0 → 2.29.1

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 (135) hide show
  1. package/README.md +71 -71
  2. package/dist/analytics.js +35 -33
  3. package/dist/api.js +34 -34
  4. package/dist/auth.js +50 -44
  5. package/dist/cli.js +2 -2
  6. package/dist/commands/auth.js +58 -52
  7. package/dist/commands/bootstrap.js +115 -157
  8. package/dist/commands/branches.js +154 -147
  9. package/dist/commands/bucket.js +124 -118
  10. package/dist/commands/checkout.js +49 -49
  11. package/dist/commands/config.js +212 -88
  12. package/dist/commands/connection_string.js +62 -62
  13. package/dist/commands/data_api.js +96 -96
  14. package/dist/commands/databases.js +23 -23
  15. package/dist/commands/deploy.js +12 -12
  16. package/dist/commands/dev.js +114 -114
  17. package/dist/commands/env.js +43 -43
  18. package/dist/commands/functions.js +97 -98
  19. package/dist/commands/index.js +26 -26
  20. package/dist/commands/init.js +23 -22
  21. package/dist/commands/ip_allow.js +29 -29
  22. package/dist/commands/link.js +223 -166
  23. package/dist/commands/neon_auth.js +381 -363
  24. package/dist/commands/operations.js +11 -11
  25. package/dist/commands/orgs.js +8 -8
  26. package/dist/commands/projects.js +101 -99
  27. package/dist/commands/psql.js +31 -31
  28. package/dist/commands/roles.js +21 -21
  29. package/dist/commands/schema_diff.js +23 -23
  30. package/dist/commands/set_context.js +17 -17
  31. package/dist/commands/status.js +17 -17
  32. package/dist/commands/user.js +5 -5
  33. package/dist/commands/vpc_endpoints.js +50 -50
  34. package/dist/config.js +7 -7
  35. package/dist/config_format.js +5 -5
  36. package/dist/context.js +23 -16
  37. package/dist/current_branch_fast_path.js +6 -6
  38. package/dist/dev/env.js +34 -34
  39. package/dist/dev/functions.js +4 -4
  40. package/dist/dev/inputs.js +6 -6
  41. package/dist/dev/runtime.js +25 -25
  42. package/dist/env.js +14 -14
  43. package/dist/env_file.js +13 -13
  44. package/dist/errors.js +19 -19
  45. package/dist/functions_api.js +10 -10
  46. package/dist/help.js +15 -15
  47. package/dist/index.js +94 -92
  48. package/dist/log.js +2 -2
  49. package/dist/pkg.js +5 -5
  50. package/dist/psql/cli.js +4 -2
  51. package/dist/psql/command/cmd_cond.js +61 -61
  52. package/dist/psql/command/cmd_connect.js +159 -154
  53. package/dist/psql/command/cmd_copy.js +107 -97
  54. package/dist/psql/command/cmd_describe.js +368 -363
  55. package/dist/psql/command/cmd_format.js +276 -263
  56. package/dist/psql/command/cmd_io.js +269 -263
  57. package/dist/psql/command/cmd_lo.js +74 -66
  58. package/dist/psql/command/cmd_meta.js +148 -148
  59. package/dist/psql/command/cmd_misc.js +17 -17
  60. package/dist/psql/command/cmd_pipeline.js +142 -135
  61. package/dist/psql/command/cmd_restrict.js +25 -25
  62. package/dist/psql/command/cmd_show.js +183 -168
  63. package/dist/psql/command/dispatch.js +26 -26
  64. package/dist/psql/command/shared.js +14 -14
  65. package/dist/psql/complete/filenames.js +16 -16
  66. package/dist/psql/complete/index.js +4 -4
  67. package/dist/psql/complete/matcher.js +33 -32
  68. package/dist/psql/complete/psqlVars.js +173 -173
  69. package/dist/psql/complete/queries.js +5 -3
  70. package/dist/psql/complete/rules.js +900 -863
  71. package/dist/psql/core/common.js +136 -133
  72. package/dist/psql/core/help.js +343 -343
  73. package/dist/psql/core/mainloop.js +160 -153
  74. package/dist/psql/core/prompt.js +126 -123
  75. package/dist/psql/core/settings.js +111 -111
  76. package/dist/psql/core/sqlHelp.js +150 -150
  77. package/dist/psql/core/startup.js +211 -205
  78. package/dist/psql/core/syncVars.js +14 -14
  79. package/dist/psql/core/variables.js +24 -24
  80. package/dist/psql/describe/formatters.js +302 -289
  81. package/dist/psql/describe/processNamePattern.js +28 -28
  82. package/dist/psql/describe/queries.js +656 -651
  83. package/dist/psql/index.js +436 -411
  84. package/dist/psql/io/history.js +36 -36
  85. package/dist/psql/io/input.js +15 -15
  86. package/dist/psql/io/lineEditor/buffer.js +27 -25
  87. package/dist/psql/io/lineEditor/complete.js +15 -15
  88. package/dist/psql/io/lineEditor/filename.js +22 -22
  89. package/dist/psql/io/lineEditor/index.js +65 -62
  90. package/dist/psql/io/lineEditor/keymap.js +325 -318
  91. package/dist/psql/io/lineEditor/vt100.js +60 -60
  92. package/dist/psql/io/pgpass.js +18 -18
  93. package/dist/psql/io/pgservice.js +14 -14
  94. package/dist/psql/io/psqlrc.js +46 -46
  95. package/dist/psql/print/aligned.js +175 -166
  96. package/dist/psql/print/asciidoc.js +51 -51
  97. package/dist/psql/print/crosstab.js +34 -31
  98. package/dist/psql/print/csv.js +25 -22
  99. package/dist/psql/print/html.js +54 -54
  100. package/dist/psql/print/json.js +12 -12
  101. package/dist/psql/print/latex.js +118 -118
  102. package/dist/psql/print/pager.js +28 -26
  103. package/dist/psql/print/troff.js +48 -48
  104. package/dist/psql/print/unaligned.js +15 -14
  105. package/dist/psql/print/units.js +17 -17
  106. package/dist/psql/scanner/slash.js +48 -46
  107. package/dist/psql/scanner/sql.js +88 -84
  108. package/dist/psql/scanner/stringutils.js +21 -17
  109. package/dist/psql/types/index.js +7 -7
  110. package/dist/psql/types/scanner.js +8 -8
  111. package/dist/psql/wire/connection.js +341 -327
  112. package/dist/psql/wire/copy.js +7 -7
  113. package/dist/psql/wire/pipeline.js +26 -24
  114. package/dist/psql/wire/protocol.js +102 -102
  115. package/dist/psql/wire/sasl.js +62 -62
  116. package/dist/psql/wire/tls.js +79 -73
  117. package/dist/storage_api.js +15 -15
  118. package/dist/test_utils/fixtures.js +34 -31
  119. package/dist/test_utils/oauth_server.js +5 -5
  120. package/dist/utils/api_enums.js +13 -13
  121. package/dist/utils/branch_notice.js +5 -5
  122. package/dist/utils/branch_picker.js +26 -26
  123. package/dist/utils/compute_units.js +4 -4
  124. package/dist/utils/enrichers.js +20 -15
  125. package/dist/utils/esbuild.js +28 -28
  126. package/dist/utils/formats.js +1 -1
  127. package/dist/utils/middlewares.js +3 -3
  128. package/dist/utils/package_manager.js +68 -0
  129. package/dist/utils/point_in_time.js +12 -12
  130. package/dist/utils/psql.js +30 -30
  131. package/dist/utils/string.js +2 -2
  132. package/dist/utils/ui.js +9 -9
  133. package/dist/utils/zip.js +1 -1
  134. package/dist/writer.js +17 -17
  135. package/package.json +6 -7
package/dist/api.js CHANGED
@@ -11,29 +11,29 @@
11
11
  // {@link isNeonApiError} and reads `error.status` / `error.data`. There is no
12
12
  // axios anywhere in neonctl: requests go through the global `fetch`, and this is
13
13
  // the one place HTTP errors are shaped.
14
- import { Readable } from 'node:stream';
15
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
16
- import { createClient, createConfig } from '@neon/sdk/raw';
17
- import * as raw from '@neon/sdk/raw';
18
- import { log } from './log.js';
19
- import pkg from './pkg.js';
14
+ import { Readable } from "node:stream";
15
+ import * as raw from "@neon/sdk/raw";
16
+ import { createClient, createConfig } from "@neon/sdk/raw";
17
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
18
+ import { log } from "./log.js";
19
+ import pkg from "./pkg.js";
20
20
  // Node's global `fetch` (undici) ignores HTTP_PROXY / HTTPS_PROXY / NO_PROXY,
21
21
  // whereas the axios-based client neonctl used previously honored them. Restore
22
22
  // that behaviour by installing a proxy-aware global dispatcher — but only when a
23
23
  // proxy is actually configured, so the default (no-proxy) path stays untouched.
24
24
  // This covers every `fetch` neonctl makes, including the direct S3 upload.
25
25
  const PROXY_ENV_VARS = [
26
- 'HTTP_PROXY',
27
- 'http_proxy',
28
- 'HTTPS_PROXY',
29
- 'https_proxy',
30
- 'ALL_PROXY',
31
- 'all_proxy',
26
+ "HTTP_PROXY",
27
+ "http_proxy",
28
+ "HTTPS_PROXY",
29
+ "https_proxy",
30
+ "ALL_PROXY",
31
+ "all_proxy",
32
32
  ];
33
33
  if (PROXY_ENV_VARS.some((name) => process.env[name])) {
34
34
  setGlobalDispatcher(new EnvHttpProxyAgent());
35
35
  }
36
- const DEFAULT_API_HOST = 'https://console.neon.tech/api/v2';
36
+ const DEFAULT_API_HOST = "https://console.neon.tech/api/v2";
37
37
  const REQUEST_TIMEOUT_MS = 60000;
38
38
  const USER_AGENT = `neonctl v${pkg.version}`;
39
39
  /** Mirrors the api-client `ContentType` enum used by the `request()` escape hatch. */
@@ -53,7 +53,7 @@ export var ContentType;
53
53
  export class NeonApiError extends Error {
54
54
  constructor(message, init = {}) {
55
55
  super(message);
56
- this.name = 'NeonApiError';
56
+ this.name = "NeonApiError";
57
57
  this.status = init.status;
58
58
  this.statusText = init.statusText;
59
59
  this.data = init.data;
@@ -68,18 +68,18 @@ export function isNeonApiError(err) {
68
68
  }
69
69
  /** Extract a `message` string from a parsed error body, if present. */
70
70
  export function messageFromBody(body) {
71
- if (body && typeof body === 'object' && 'message' in body) {
71
+ if (body && typeof body === "object" && "message" in body) {
72
72
  const message = body.message;
73
- if (typeof message === 'string')
73
+ if (typeof message === "string")
74
74
  return message;
75
75
  }
76
76
  return undefined;
77
77
  }
78
78
  /** Extract a machine-readable `code` string from a parsed error body, if present. */
79
79
  export function codeFromBody(body) {
80
- if (body && typeof body === 'object' && 'code' in body) {
80
+ if (body && typeof body === "object" && "code" in body) {
81
81
  const code = body.code;
82
- if (typeof code === 'string')
82
+ if (typeof code === "string")
83
83
  return code;
84
84
  }
85
85
  return undefined;
@@ -113,7 +113,7 @@ function headersToObject(headers) {
113
113
  }
114
114
  function isAbortError(err) {
115
115
  return (err instanceof Error &&
116
- (err.name === 'AbortError' || err.name === 'TimeoutError'));
116
+ (err.name === "AbortError" || err.name === "TimeoutError"));
117
117
  }
118
118
  /**
119
119
  * Walk an error's `cause` chain to find the underlying socket/DNS `code` (e.g.
@@ -124,9 +124,9 @@ function isAbortError(err) {
124
124
  function readSocketCode(err) {
125
125
  let current = err;
126
126
  for (let depth = 0; depth < 6 && current != null; depth++) {
127
- if (typeof current === 'object' && 'code' in current) {
127
+ if (typeof current === "object" && "code" in current) {
128
128
  const code = current.code;
129
- if (typeof code === 'string')
129
+ if (typeof code === "string")
130
130
  return code;
131
131
  }
132
132
  current = current.cause;
@@ -160,10 +160,10 @@ function httpError(response, body) {
160
160
  */
161
161
  function networkError(err) {
162
162
  if (isAbortError(err)) {
163
- return new NeonApiError('Request timed out', { code: 'ECONNABORTED' });
163
+ return new NeonApiError("Request timed out", { code: "ECONNABORTED" });
164
164
  }
165
165
  return new NeonApiError(err instanceof Error ? err.message : String(err), {
166
- code: readSocketCode(err) ?? 'ENETWORK',
166
+ code: readSocketCode(err) ?? "ENETWORK",
167
167
  });
168
168
  }
169
169
  /**
@@ -176,11 +176,11 @@ const timedFetch = async (input, init) => {
176
176
  const signal = init?.signal
177
177
  ? AbortSignal.any([init.signal, timeout])
178
178
  : timeout;
179
- const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
179
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
180
180
  const url = input instanceof Request ? input.url : String(input);
181
- log.debug('%s %s', method.toUpperCase(), url);
181
+ log.debug("%s %s", method.toUpperCase(), url);
182
182
  const response = await fetch(input, { ...init, signal });
183
- log.debug('%d %s', response.status, response.statusText);
183
+ log.debug("%d %s", response.status, response.statusText);
184
184
  return response;
185
185
  };
186
186
  const RETRY_COUNT = 5;
@@ -211,7 +211,7 @@ export const retryOnLock = async (fn) => {
211
211
  throw errOut;
212
212
  };
213
213
  function buildUrl(apiHost, path, query) {
214
- const url = new URL(`${apiHost.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`);
214
+ const url = new URL(`${apiHost.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`);
215
215
  if (query) {
216
216
  for (const [key, value] of Object.entries(query)) {
217
217
  if (value === undefined || value === null)
@@ -223,7 +223,7 @@ function buildUrl(apiHost, path, query) {
223
223
  }
224
224
  async function readJsonBody(response) {
225
225
  const text = await response.text();
226
- if (text.trim() === '')
226
+ if (text.trim() === "")
227
227
  return undefined;
228
228
  try {
229
229
  return JSON.parse(text);
@@ -241,7 +241,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
241
241
  auth: () => apiKey,
242
242
  baseUrl,
243
243
  fetch: timedFetch,
244
- headers: { 'User-Agent': USER_AGENT },
244
+ headers: { "User-Agent": USER_AGENT },
245
245
  }));
246
246
  /** Await a raw call, unwrap to a `{ data, status, headers }` envelope, or throw {@link NeonApiError}. */
247
247
  async function call(run) {
@@ -254,7 +254,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
254
254
  }
255
255
  const response = result.response;
256
256
  if (!response) {
257
- throw networkError(result.error ?? new Error('No response from Neon API'));
257
+ throw networkError(result.error ?? new Error("No response from Neon API"));
258
258
  }
259
259
  if (!response.ok) {
260
260
  throw httpError(response, result.error ?? result.data);
@@ -274,7 +274,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
274
274
  */
275
275
  async function request(params) {
276
276
  const url = buildUrl(baseUrl, params.path, params.query);
277
- const headers = { 'User-Agent': USER_AGENT };
277
+ const headers = { "User-Agent": USER_AGENT };
278
278
  if (params.secure !== false) {
279
279
  headers.Authorization = `Bearer ${apiKey}`;
280
280
  }
@@ -284,7 +284,7 @@ export const getApiClient = ({ apiKey, apiHost }) => {
284
284
  payload = params.body;
285
285
  }
286
286
  else if (params.body !== undefined) {
287
- headers['Content-Type'] = ContentType.Json;
287
+ headers["Content-Type"] = ContentType.Json;
288
288
  payload = JSON.stringify(params.body);
289
289
  }
290
290
  let response;
@@ -301,13 +301,13 @@ export const getApiClient = ({ apiKey, apiHost }) => {
301
301
  if (!response.ok) {
302
302
  // For a streamed download the error body arrives as a stream too; hand it
303
303
  // back as a Node `Readable` so the caller can drain it for a message.
304
- const errorBody = params.format === 'stream' && response.body
304
+ const errorBody = params.format === "stream" && response.body
305
305
  ? webStreamToNodeReadable(response.body)
306
306
  : await readJsonBody(response);
307
307
  throw httpError(response, errorBody);
308
308
  }
309
309
  let data;
310
- if (params.format === 'stream') {
310
+ if (params.format === "stream") {
311
311
  data = response.body
312
312
  ? webStreamToNodeReadable(response.body)
313
313
  : undefined;
package/dist/auth.js CHANGED
@@ -1,58 +1,62 @@
1
- import * as client from 'openid-client';
2
- import { createServer } from 'node:http';
3
- import { createReadStream } from 'node:fs';
4
- import { join } from 'node:path';
5
- import open from 'open';
6
- import { log } from './log.js';
7
- import { fileURLToPath } from 'node:url';
8
- import { sendError } from './analytics.js';
9
- import { matchErrorCode } from './errors.js';
10
- import { extendTokenSet } from './utils/auth.js';
1
+ import { createReadStream } from "node:fs";
2
+ import { createServer, } from "node:http";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import open from "open";
6
+ import * as client from "openid-client";
7
+ import { sendError } from "./analytics.js";
8
+ import { matchErrorCode } from "./errors.js";
9
+ import { log } from "./log.js";
10
+ import { extendTokenSet } from "./utils/auth.js";
11
11
  // oauth server timeouts
12
12
  const SERVER_TIMEOUT = 10000;
13
13
  // where to wait for incoming redirect request from oauth server to arrive
14
14
  const REDIRECT_URI = (port) => `http://127.0.0.1:${port}/callback`;
15
15
  // These scopes cannot be cancelled, they are always needed.
16
- const ALWAYS_PRESENT_SCOPES = ['openid', 'offline', 'offline_access'];
16
+ const ALWAYS_PRESENT_SCOPES = ["openid", "offline", "offline_access"];
17
17
  const NEONCTL_SCOPES = [
18
18
  ...ALWAYS_PRESENT_SCOPES,
19
- 'urn:neoncloud:projects:create',
20
- 'urn:neoncloud:projects:read',
21
- 'urn:neoncloud:projects:update',
22
- 'urn:neoncloud:projects:delete',
23
- 'urn:neoncloud:orgs:create',
24
- 'urn:neoncloud:orgs:read',
25
- 'urn:neoncloud:orgs:update',
26
- 'urn:neoncloud:orgs:delete',
27
- 'urn:neoncloud:orgs:permission',
19
+ "urn:neoncloud:projects:create",
20
+ "urn:neoncloud:projects:read",
21
+ "urn:neoncloud:projects:update",
22
+ "urn:neoncloud:projects:delete",
23
+ "urn:neoncloud:orgs:create",
24
+ "urn:neoncloud:orgs:read",
25
+ "urn:neoncloud:orgs:update",
26
+ "urn:neoncloud:orgs:delete",
27
+ "urn:neoncloud:orgs:permission",
28
28
  ];
29
29
  const AUTH_TIMEOUT_SECONDS = 60;
30
- export const defaultClientID = 'neonctl';
30
+ export const defaultClientID = "neonctl";
31
31
  export const refreshToken = async ({ oauthHost, clientId, allowUnsafeTls }, tokenSet) => {
32
- log.debug('Discovering oauth server');
33
- const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: 'none' }, client.None(), {
32
+ log.debug("Discovering oauth server");
33
+ const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
34
34
  timeout: SERVER_TIMEOUT,
35
- // eslint-disable-next-line @typescript-eslint/no-deprecated
36
- execute: allowUnsafeTls ? [client.allowInsecureRequests] : undefined,
35
+ execute: allowUnsafeTls
36
+ ? // eslint-disable-next-line @typescript-eslint/no-deprecated
37
+ [client.allowInsecureRequests]
38
+ : undefined,
37
39
  });
38
40
  return await client.refreshTokenGrant(configuration, tokenSet.refresh_token);
39
41
  };
40
42
  export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
41
- log.debug('Discovering oauth server');
42
- const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: 'none' }, client.None(), {
43
+ log.debug("Discovering oauth server");
44
+ const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
43
45
  timeout: SERVER_TIMEOUT,
44
- // eslint-disable-next-line @typescript-eslint/no-deprecated
45
- execute: allowUnsafeTls ? [client.allowInsecureRequests] : undefined,
46
+ execute: allowUnsafeTls
47
+ ? // eslint-disable-next-line @typescript-eslint/no-deprecated
48
+ [client.allowInsecureRequests]
49
+ : undefined,
46
50
  });
47
51
  //
48
52
  // Start HTTP server and wait till /callback is hit
49
53
  //
50
- log.debug('Starting HTTP Server for callback');
54
+ log.debug("Starting HTTP Server for callback");
51
55
  const server = createServer();
52
- server.listen(0, '127.0.0.1', function () {
56
+ server.listen(0, "127.0.0.1", function () {
53
57
  log.debug(`Listening on port ${this.address().port}`);
54
58
  });
55
- await new Promise((resolve) => server.once('listening', resolve));
59
+ await new Promise((resolve) => server.once("listening", resolve));
56
60
  const listen_port = server.address().port;
57
61
  // https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
58
62
  const state = client.randomState();
@@ -67,17 +71,17 @@ export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
67
71
  //
68
72
  // Wait for callback and follow oauth flow.
69
73
  //
70
- if (!request.url?.startsWith('/callback')) {
74
+ if (!request.url?.startsWith("/callback")) {
71
75
  response.writeHead(404);
72
76
  response.end();
73
77
  return;
74
78
  }
75
79
  // process the CORS preflight OPTIONS request
76
- if (request.method === 'OPTIONS') {
80
+ if (request.method === "OPTIONS") {
77
81
  response.writeHead(200, {
78
- 'Access-Control-Allow-Origin': '*',
79
- 'Access-Control-Allow-Methods': 'GET, POST',
80
- 'Access-Control-Allow-Headers': 'Content-Type',
82
+ "Access-Control-Allow-Origin": "*",
83
+ "Access-Control-Allow-Methods": "GET, POST",
84
+ "Access-Control-Allow-Headers": "Content-Type",
81
85
  });
82
86
  response.end();
83
87
  return;
@@ -87,29 +91,31 @@ export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
87
91
  pkceCodeVerifier: codeVerifier,
88
92
  expectedState: state,
89
93
  });
90
- response.writeHead(200, { 'Content-Type': 'text/html' });
91
- createReadStream(join(fileURLToPath(new URL('.', import.meta.url)), './callback.html')).pipe(response);
94
+ response.writeHead(200, { "Content-Type": "text/html" });
95
+ createReadStream(join(fileURLToPath(new URL(".", import.meta.url)), "./callback.html")).pipe(response);
92
96
  clearTimeout(timer);
93
97
  const exp = new Date();
94
98
  exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
95
99
  resolve(extendTokenSet(tokenSet));
96
100
  server.close();
97
101
  };
98
- server.on('request', (req, res) => {
102
+ server.on("request", (req, res) => {
99
103
  void onRequest(req, res);
100
104
  });
101
105
  //
102
106
  // Open browser to let user authenticate
103
107
  //
104
- const scopes = clientId == defaultClientID ? NEONCTL_SCOPES : ALWAYS_PRESENT_SCOPES;
108
+ const scopes = clientId == defaultClientID
109
+ ? NEONCTL_SCOPES
110
+ : ALWAYS_PRESENT_SCOPES;
105
111
  const authUrl = client.buildAuthorizationUrl(configuration, {
106
- scope: scopes.join(' '),
112
+ scope: scopes.join(" "),
107
113
  state,
108
114
  code_challenge: codeChallenge,
109
- code_challenge_method: 'S256',
115
+ code_challenge_method: "S256",
110
116
  redirect_uri: REDIRECT_URI(listen_port),
111
117
  });
112
- log.info('Awaiting authentication in web browser.');
118
+ log.info("Awaiting authentication in web browser.");
113
119
  log.info(`Auth Url: ${authUrl}`);
114
120
  open(authUrl.href).catch((err) => {
115
121
  const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { tryCurrentBranchFastPath } from './current_branch_fast_path.js';
2
+ import { tryCurrentBranchFastPath } from "./current_branch_fast_path.js";
3
3
  // Fast path for the offline `(config) status --current-branch` probe (used by shell
4
4
  // prompts): read the pinned branch from `.neon` without loading the full command tree,
5
5
  // api-client, and yargs (~200ms). Falls through to the full CLI for everything else, so
6
6
  // the heavy `index.js` is imported lazily and only when actually needed.
7
7
  if (!tryCurrentBranchFastPath(process.argv)) {
8
- void import('./index.js');
8
+ void import("./index.js");
9
9
  }
@@ -1,26 +1,26 @@
1
- import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { getApiClient } from '../api.js';
5
- import { auth, refreshToken } from '../auth.js';
6
- import { isCurrentBranchProbe } from '../context.js';
7
- import { CREDENTIALS_FILE } from '../config.js';
8
- import { isCi } from '../env.js';
9
- import { log } from '../log.js';
10
- import { extendTokenSet } from '../utils/auth.js';
11
- export const command = 'auth';
12
- export const aliases = ['login'];
13
- export const describe = 'Authenticate';
14
- export const builder = (yargs) => yargs.option('context-file', {
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getApiClient } from "../api.js";
5
+ import { auth, refreshToken } from "../auth.js";
6
+ import { CREDENTIALS_FILE } from "../config.js";
7
+ import { isConfigInit, isCurrentBranchProbe } from "../context.js";
8
+ import { isCi } from "../env.js";
9
+ import { log } from "../log.js";
10
+ import { extendTokenSet } from "../utils/auth.js";
11
+ export const command = "auth";
12
+ export const aliases = ["login"];
13
+ export const describe = "Authenticate";
14
+ export const builder = (yargs) => yargs.option("context-file", {
15
15
  hidden: true,
16
16
  });
17
17
  export const handler = async (args) => {
18
18
  await authFlow(args);
19
19
  };
20
- export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, 'force-auth': forceAuthKebab, allowUnsafeTls, }) => {
20
+ export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, "force-auth": forceAuthKebab, allowUnsafeTls, }) => {
21
21
  const allowInteractiveAuth = forceAuth ?? forceAuthKebab;
22
22
  if (!allowInteractiveAuth && isCi()) {
23
- throw new Error('Cannot run interactive auth in CI');
23
+ throw new Error("Cannot run interactive auth in CI");
24
24
  }
25
25
  const tokenSet = await auth({
26
26
  oauthHost: oauthHost,
@@ -30,16 +30,16 @@ export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceA
30
30
  const credentialsPath = join(configDir, CREDENTIALS_FILE);
31
31
  try {
32
32
  await preserveCredentials(credentialsPath, tokenSet, getApiClient({
33
- apiKey: tokenSet.access_token || '',
33
+ apiKey: tokenSet.access_token || "",
34
34
  apiHost,
35
35
  }));
36
36
  }
37
37
  catch {
38
- log.error('Failed to save credentials');
39
- return '';
38
+ log.error("Failed to save credentials");
39
+ return "";
40
40
  }
41
- log.info('Auth complete');
42
- return tokenSet.access_token || '';
41
+ log.info("Auth complete");
42
+ return tokenSet.access_token || "";
43
43
  };
44
44
  const preserveCredentials = async (path, credentials, apiClient) => {
45
45
  const { data: { id }, } = await apiClient.getCurrentUserInfo();
@@ -52,13 +52,13 @@ const preserveCredentials = async (path, credentials, apiClient) => {
52
52
  writeFileSync(path, contents, {
53
53
  mode: 0o700,
54
54
  });
55
- log.debug('Saved credentials to %s', path);
56
- log.debug('Credentials MD5 hash: %s', md5hash(contents));
55
+ log.debug("Saved credentials to %s", path);
56
+ log.debug("Credentials MD5 hash: %s", md5hash(contents));
57
57
  };
58
58
  const handleExistingToken = async (tokenSet, props, credentialsPath) => {
59
59
  // Use existing access_token, if present and valid
60
60
  if (tokenSet.access_token && tokenSet.expires_at > Date.now()) {
61
- log.debug('Using existing valid access_token');
61
+ log.debug("Using existing valid access_token");
62
62
  const apiClient = getApiClient({
63
63
  apiKey: tokenSet.access_token,
64
64
  apiHost: props.apiHost,
@@ -67,10 +67,10 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
67
67
  }
68
68
  // Either access_token is missing or its expired. Refresh the token
69
69
  log.debug(tokenSet.expires_at < Date.now()
70
- ? 'Token is expired, attempting refresh'
71
- : 'Token is missing access_token, attempting refresh');
70
+ ? "Token is expired, attempting refresh"
71
+ : "Token is missing access_token, attempting refresh");
72
72
  if (!tokenSet.refresh_token) {
73
- log.debug('TokenSet is missing refresh_token, starting authentication');
73
+ log.debug("TokenSet is missing refresh_token, starting authentication");
74
74
  return null;
75
75
  }
76
76
  try {
@@ -87,13 +87,13 @@ const handleExistingToken = async (tokenSet, props, credentialsPath) => {
87
87
  apiHost: props.apiHost,
88
88
  });
89
89
  await preserveCredentials(credentialsPath, extendedTokenSet, apiClient);
90
- log.debug('Token refresh successful');
90
+ log.debug("Token refresh successful");
91
91
  return { apiKey, apiClient };
92
92
  }
93
93
  catch (err) {
94
- const typedErr = err instanceof Error ? err : new Error('Unknown error');
95
- log.debug('Failed to refresh token: %s', typedErr.message);
96
- throw new Error('AUTH_REFRESH_FAILED');
94
+ const typedErr = err instanceof Error ? err : new Error("Unknown error");
95
+ log.debug("Failed to refresh token: %s", typedErr.message);
96
+ throw new Error("AUTH_REFRESH_FAILED");
97
97
  }
98
98
  };
99
99
  export const ensureAuth = async (props) => {
@@ -107,23 +107,28 @@ export const ensureAuth = async (props) => {
107
107
  if (isCurrentBranchProbe(props)) {
108
108
  return;
109
109
  }
110
+ // `config init` only scaffolds a neon.ts and installs npm packages locally; it
111
+ // never calls the Neon API, so skip auth entirely — no token refresh, no login.
112
+ if (isConfigInit(props)) {
113
+ return;
114
+ }
110
115
  // `dev` runs a function locally. It injects the selected branch's env vars
111
116
  // when credentials happen to be available, but must never trigger an
112
117
  // interactive login: use an API key or existing stored credentials if
113
118
  // present, otherwise run with no API client (env injection is skipped).
114
- const isLocalDev = props._[0] === 'dev';
119
+ const isLocalDev = props._[0] === "dev";
115
120
  // `bootstrap` only copies a public template repo; it never calls the Neon
116
121
  // API, so it must work without credentials and must never pop a browser
117
122
  // login. It uses an API key / stored credentials when present (harmless),
118
123
  // otherwise it proceeds with no API client.
119
- const isBootstrap = props._[0] === 'bootstrap';
124
+ const isBootstrap = props._[0] === "bootstrap";
120
125
  // `init` manages its own auth flow (asks the user if they have an account,
121
126
  // then triggers OAuth at the right time). Skip the global auth middleware.
122
- const isInit = props._[0] === 'init';
127
+ const isInit = props._[0] === "init";
123
128
  // Use existing API key or handle auth command
124
- if (props.apiKey || props._[0] === 'auth') {
129
+ if (props.apiKey || props._[0] === "auth") {
125
130
  if (props.apiKey) {
126
- log.debug('Using an API key to authorize requests');
131
+ log.debug("Using an API key to authorize requests");
127
132
  }
128
133
  props.apiClient = getApiClient({
129
134
  apiKey: props.apiKey,
@@ -134,10 +139,10 @@ export const ensureAuth = async (props) => {
134
139
  const credentialsPath = join(props.configDir, CREDENTIALS_FILE);
135
140
  // Handle case when credentials file exists
136
141
  if (existsSync(credentialsPath)) {
137
- log.debug('Trying to read credentials from %s', credentialsPath);
142
+ log.debug("Trying to read credentials from %s", credentialsPath);
138
143
  try {
139
- const contents = readFileSync(credentialsPath, 'utf8');
140
- log.debug('Credentials MD5 hash: %s', md5hash(contents));
144
+ const contents = readFileSync(credentialsPath, "utf8");
145
+ log.debug("Credentials MD5 hash: %s", md5hash(contents));
141
146
  const tokenSet = JSON.parse(contents);
142
147
  // Try to use existing token or refresh it
143
148
  const result = await handleExistingToken(tokenSet, props, credentialsPath);
@@ -148,32 +153,33 @@ export const ensureAuth = async (props) => {
148
153
  }
149
154
  }
150
155
  catch (err) {
151
- if (!(err instanceof Error && err.message === 'AUTH_REFRESH_FAILED') &&
152
- err.code !== 'ENOENT' &&
156
+ if (!(err instanceof Error &&
157
+ err.message === "AUTH_REFRESH_FAILED") &&
158
+ err.code !== "ENOENT" &&
153
159
  !(err instanceof SyntaxError)) {
154
160
  // Throw for any errors except auth refresh failure, missing file, or invalid credentials file
155
161
  throw err;
156
162
  }
157
163
  // Fall through to new auth flow for auth failures
158
- log.debug('Ensure auth failed, starting authentication', err);
164
+ log.debug("Ensure auth failed, starting authentication", err);
159
165
  }
160
166
  }
161
167
  else {
162
- log.debug('Credentials file %s does not exist, starting authentication', credentialsPath);
168
+ log.debug("Credentials file %s does not exist, starting authentication", credentialsPath);
163
169
  }
164
170
  // `dev` never launches the interactive browser flow. With no usable
165
171
  // credentials it proceeds without an API client; env injection is skipped
166
172
  // and the function still runs locally.
167
173
  if (isLocalDev) {
168
- log.debug('dev: no usable credentials; running without env injection');
174
+ log.debug("dev: no usable credentials; running without env injection");
169
175
  return;
170
176
  }
171
177
  if (isBootstrap) {
172
- log.debug('bootstrap: no usable credentials; continuing without auth');
178
+ log.debug("bootstrap: no usable credentials; continuing without auth");
173
179
  return;
174
180
  }
175
181
  if (isInit) {
176
- log.debug('init: skipping global auth; init manages its own auth flow');
182
+ log.debug("init: skipping global auth; init manages its own auth flow");
177
183
  return;
178
184
  }
179
185
  // Start new auth flow if no valid token exists or refresh failed
@@ -193,16 +199,16 @@ export const deleteCredentials = (configDir) => {
193
199
  try {
194
200
  if (existsSync(credentialsPath)) {
195
201
  rmSync(credentialsPath);
196
- log.info('Deleted credentials from %s', credentialsPath);
202
+ log.info("Deleted credentials from %s", credentialsPath);
197
203
  }
198
204
  else {
199
- log.debug('Credentials file %s does not exist', credentialsPath);
205
+ log.debug("Credentials file %s does not exist", credentialsPath);
200
206
  }
201
207
  }
202
208
  catch (err) {
203
- const typedErr = err instanceof Error ? err : new Error('Unknown error');
204
- log.error('Failed to delete credentials: %s', typedErr.message);
205
- throw new Error('CREDENTIALS_DELETE_FAILED');
209
+ const typedErr = err instanceof Error ? err : new Error("Unknown error");
210
+ log.error("Failed to delete credentials: %s", typedErr.message);
211
+ throw new Error("CREDENTIALS_DELETE_FAILED");
206
212
  }
207
213
  };
208
- const md5hash = (s) => createHash('md5').update(s).digest('hex');
214
+ const md5hash = (s) => createHash("md5").update(s).digest("hex");