neon 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +70 -30
  2. package/dist/_chunks/{auth_selection-DGgq6ifc.js → auth_selection-CV_3n1du.js} +1 -1
  3. package/dist/_chunks/{cmd_pipeline-CUbBO9U_.js → cmd_pipeline-CEk93dGN.js} +8 -6
  4. package/dist/_chunks/credential_io-D2vw1UOw.js +409 -0
  5. package/dist/_chunks/{paths-DMq0Lt7a.js → paths-g3DRqJiD.js} +1 -1
  6. package/dist/_chunks/{profiles-Ir29rqns.js → profiles-CVXNvU2r.js} +106 -66
  7. package/dist/_chunks/{psql-DWH-kc69.js → psql-BNljLda2.js} +8 -7
  8. package/dist/_chunks/{rolldown-runtime-D7D4PA-g.js → rolldown-runtime-8H4AJuhK.js} +1 -0
  9. package/dist/analytics.js +19 -5
  10. package/dist/auth_context.js +25 -24
  11. package/dist/commands/api.js +1 -1
  12. package/dist/commands/api_keys.js +1 -1
  13. package/dist/commands/auth.js +121 -80
  14. package/dist/commands/bootstrap.js +2 -2
  15. package/dist/commands/branches.js +1 -1
  16. package/dist/commands/bucket.js +1 -1
  17. package/dist/commands/checkout.js +1 -1
  18. package/dist/commands/config.js +1 -1
  19. package/dist/commands/connection_string.js +1 -1
  20. package/dist/commands/data_api.js +1 -1
  21. package/dist/commands/databases.js +1 -1
  22. package/dist/commands/deploy.js +1 -1
  23. package/dist/commands/dev.js +1 -1
  24. package/dist/commands/diff.js +1 -1
  25. package/dist/commands/env.js +1 -1
  26. package/dist/commands/functions.js +1 -1
  27. package/dist/commands/init.js +2 -2
  28. package/dist/commands/inspect.js +1 -1
  29. package/dist/commands/ip_allow.js +1 -1
  30. package/dist/commands/link.js +1 -1
  31. package/dist/commands/logs.js +1 -1
  32. package/dist/commands/neon_auth.js +1 -1
  33. package/dist/commands/open.js +1 -1
  34. package/dist/commands/operations.js +1 -1
  35. package/dist/commands/orgs.js +1 -1
  36. package/dist/commands/profile.js +183 -202
  37. package/dist/commands/projects.js +1 -1
  38. package/dist/commands/psql.js +1 -1
  39. package/dist/commands/roles.js +1 -1
  40. package/dist/commands/schema_diff.js +15 -13
  41. package/dist/commands/set_context.js +1 -1
  42. package/dist/commands/snapshots.js +1 -1
  43. package/dist/commands/status.js +1 -1
  44. package/dist/commands/user.js +1 -1
  45. package/dist/commands/vpc_endpoints.js +1 -1
  46. package/dist/config.js +1 -1
  47. package/dist/credential_io.js +2 -0
  48. package/dist/index.js +27 -25
  49. package/dist/init/auth.js +14 -25
  50. package/dist/keyring.js +42 -0
  51. package/dist/psql/cli.js +1 -1
  52. package/dist/psql/command/cmd_connect.js +4 -3
  53. package/dist/psql/command/cmd_copy.js +65 -62
  54. package/dist/psql/command/cmd_io.js +1 -1
  55. package/dist/psql/command/cmd_pipeline.js +1 -1
  56. package/dist/psql/command/dispatch.js +1 -1
  57. package/dist/psql/core/common.js +1 -1
  58. package/dist/psql/core/mainloop.js +1 -1
  59. package/dist/psql/core/startup.js +1 -1
  60. package/dist/psql/index.js +1 -1
  61. package/dist/psql/io/psqlrc.js +13 -11
  62. package/dist/psql/print/aligned.js +9 -5
  63. package/dist/psql/wire/tls.js +11 -9
  64. package/dist/retire_credential.js +86 -0
  65. package/dist/utils/inspect_db.js +1 -1
  66. package/dist/utils/middlewares.js +1 -1
  67. package/package.json +5 -4
  68. package/dist/_chunks/credentials-MYdHdKah.js +0 -188
  69. package/dist/_chunks/secure_file-BucZj4yQ.js +0 -39
@@ -1,55 +1,67 @@
1
- import { t as writeSecretFile } from "./secure_file-BucZj4yQ.js";
2
- import { n as credentialsPath, o as resolveConfigFile, r as defaultDir } from "./paths-DMq0Lt7a.js";
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { isAbsolute, relative, resolve } from "node:path";
5
- //#region ../../internals/cli-core/dist/profiles.js
1
+ import { i as defaultDir, r as credentialsPath, s as resolveConfigFile } from "./paths-g3DRqJiD.js";
2
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ //#region ../../internals/cli-core/dist/secure_file.js
6
5
  /**
7
- * # Profiles — several Neon accounts in one config directory
8
- *
9
- * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
10
- * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
11
- * "active profile" state to fall out of sync, and no migration.
12
- *
13
- * ```
14
- * ~/.config/neon/
15
- * ├── credentials.json # this IS the DEFAULT profile, not a copy of it
16
- * ├── credentials.work.json # created by `neon auth --profile work`
17
- * └── profiles.json # created only once a second profile exists
18
- * ```
19
- *
20
- * `profiles.json` maps a name to a path, and the path may point anywhere — which is what
21
- * makes adopting an existing directory a one-line edit rather than an import command:
6
+ * Write a secret to disk owner-only, by creating a temporary file in the same directory and
7
+ * renaming it over the target.
22
8
  *
23
- * ```json
24
- * {
25
- * "version": 1,
26
- * "profiles": {
27
- * "DEFAULT": { "credentials": "credentials.json" },
28
- * "work": {
29
- * "credentials": "../neonctl-databricks/credentials.json",
30
- * "label": "someone@example.com"
31
- * }
32
- * }
33
- * }
34
- * ```
9
+ * The rename is what makes this correct rather than merely tidy. `writeFileSync`'s `mode`
10
+ * applies only when it *creates* the file, so writing over an existing credentials file
11
+ * leaves whatever permissions it already had — a file created `0700` by an older release
12
+ * stays `0700` forever, and one created before a umask change stays world-readable. Renaming
13
+ * a fresh inode into place means every write lands at {@link SECRET_FILE_MODE}, so the
14
+ * permissions repair themselves instead of being inherited.
35
15
  *
36
- * ## Selection
16
+ * It also closes the window where a reader could see the file at default permissions: the
17
+ * temporary file is created `0600` *before* it holds the secret's final name, and `rename`
18
+ * is atomic within a directory, so there is no moment at which the target is readable by
19
+ * anyone else and no moment at which it is half-written.
37
20
  *
38
- * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
39
- * `profile use` command, so nothing persists that could disagree with what you typed.
40
- *
41
- * ## Compatibility
42
- *
43
- * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
44
- * resolves to `credentials.json` in the config directory (including an existing one in the
45
- * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
46
- * profile is, and nothing is ever moved.
21
+ * The temporary name carries the pid so two processes writing at once cannot collide on it.
22
+ */
23
+ const writeSecretFile = (path, contents) => {
24
+ const directory = dirname(path);
25
+ const temporary = join(directory, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
26
+ try {
27
+ writeFileSync(temporary, contents, {
28
+ encoding: "utf8",
29
+ mode: 384
30
+ });
31
+ renameSync(temporary, path);
32
+ } catch (err) {
33
+ try {
34
+ unlinkSync(temporary);
35
+ } catch {}
36
+ throw err;
37
+ }
38
+ };
39
+ //#endregion
40
+ //#region ../../internals/cli-core/dist/cli_config.js
41
+ const CRED_STORAGE_FILE = "file";
42
+ const CRED_STORAGE_KEYRING = "keyring";
43
+ //#endregion
44
+ //#region ../../internals/cli-core/dist/profiles.js
45
+ /**
46
+ * Pointer-only profiles avoid mirrored credentials and persistent active-profile
47
+ * state while preserving existing single-account and legacy-directory installs.
47
48
  */
48
49
  const PROFILES_FILE = "profiles.json";
50
+ const KEYRING_CREDENTIALS = "keyring";
51
+ const isKeyringPointer = (credentials) => credentials === KEYRING_CREDENTIALS;
49
52
  /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
50
53
  const DEFAULT_PROFILE = "DEFAULT";
51
54
  /** Profile names become part of a filename, so keep them boring. */
52
55
  const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
56
+ const locationOf = (profile) => profile.storage === "keyring" ? {
57
+ profile: profile.name,
58
+ storage: CRED_STORAGE_KEYRING
59
+ } : {
60
+ profile: profile.name,
61
+ storage: CRED_STORAGE_FILE,
62
+ path: profile.credentialsPath
63
+ };
64
+ const credentialsDisplay = (profile) => profile.storage === "keyring" ? KEYRING_CREDENTIALS : profile.credentialsPath;
53
65
  /** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
54
66
  const selectProfileName = (flag, env = process.env) => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? "DEFAULT";
55
67
  const assertValidProfileName = (name) => {
@@ -90,7 +102,7 @@ const inspectProfiles = (dir) => {
90
102
  if (profiles === null || typeof profiles !== "object" || Array.isArray(profiles)) return broken("it has no `profiles` object");
91
103
  for (const [name, entry] of Object.entries(profiles)) {
92
104
  if (!NAME_PATTERN.test(name)) return broken(`"${name}" is not a valid profile name`);
93
- if (entry === null || typeof entry !== "object" || typeof entry.credentials !== "string" || entry.credentials.trim() === "") return broken(`profile "${name}" has no \`credentials\` path`);
105
+ if (entry === null || typeof entry !== "object" || typeof entry.credentials !== "string" || entry.credentials.trim() === "") return broken(`profile "${name}" has no \`credentials\` pointer`);
94
106
  }
95
107
  return {
96
108
  kind: "ok",
@@ -115,7 +127,7 @@ const readProfiles = (dir, onWarn = () => {}) => {
115
127
  return null;
116
128
  };
117
129
  /**
118
- * Refuse to act on a named profile when the file that defines it cannot be read.
130
+ * Storage cannot be trusted when the profiles file cannot be read.
119
131
  *
120
132
  * Call this **before** anything that writes a credential, opens a browser, or spends an API
121
133
  * call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already
@@ -124,29 +136,39 @@ const readProfiles = (dir, onWarn = () => {}) => {
124
136
  * exists to prevent. The path resolution itself is the unsound part, since with the metadata
125
137
  * unreadable the conventional filename is a guess about which account that file belongs to.
126
138
  *
127
- * `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so
128
- * signing in normally must keep working while a broken `profiles.json` is repaired.
139
+ * DEFAULT follows the same rule because the broken file may be its only
140
+ * keyring pointer.
129
141
  */
130
142
  const assertProfilesUsable = (dir, name) => {
131
- if (name === "DEFAULT") return;
132
143
  const read = inspectProfiles(dir);
133
144
  if (read.kind === "unusable") throw new Error(`${read.reason}. Fix or delete the file before working with profile "${name}" — it is the only record of where each account's credentials live.`);
134
145
  };
135
146
  /** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
136
147
  const resolveProfile = (dir, name) => {
137
148
  const read = inspectProfiles(dir);
138
- if (read.kind === "unusable" && name !== "DEFAULT") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
149
+ if (read.kind === "unusable") throw new Error(`${read.reason}. Fix or delete the file — every profile is defined in it.`);
139
150
  const file = read.kind === "ok" ? read.file : null;
140
151
  const entry = file?.profiles[name];
141
- if (entry) return {
142
- name,
143
- credentialsPath: resolveEntryPath(dir, entry.credentials),
144
- ...entry.label ? { label: entry.label } : {},
145
- ...entry.userId ? { userId: entry.userId } : {},
146
- declared: true
147
- };
152
+ if (entry) {
153
+ if (isKeyringPointer(entry.credentials)) return {
154
+ name,
155
+ storage: CRED_STORAGE_KEYRING,
156
+ ...entry.label ? { label: entry.label } : {},
157
+ ...entry.userId ? { userId: entry.userId } : {},
158
+ declared: true
159
+ };
160
+ return {
161
+ name,
162
+ storage: CRED_STORAGE_FILE,
163
+ credentialsPath: resolveEntryPath(dir, entry.credentials),
164
+ ...entry.label ? { label: entry.label } : {},
165
+ ...entry.userId ? { userId: entry.userId } : {},
166
+ declared: true
167
+ };
168
+ }
148
169
  if (name === "DEFAULT") return {
149
170
  name,
171
+ storage: CRED_STORAGE_FILE,
150
172
  credentialsPath: credentialsPath(dir),
151
173
  declared: false
152
174
  };
@@ -171,23 +193,37 @@ const upsertProfile = (dir, name, entry) => {
171
193
  if (read.kind === "unusable") throw new Error(`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`);
172
194
  const file = read.kind === "ok" ? read.file : {
173
195
  version: 1,
174
- profiles: { [DEFAULT_PROFILE]: { credentials: relativeToProfiles(path, credentialsPath(dir)) } }
196
+ profiles: { [DEFAULT_PROFILE]: { credentials: storedPointer(path, credentialsPath(dir)) } }
175
197
  };
176
198
  file.profiles[name] = {
177
- credentials: relativeToProfiles(path, entry.credentials),
199
+ credentials: storedPointer(path, entry.credentials),
178
200
  ...entry.label ? { label: entry.label } : {},
179
201
  ...entry.userId ? { userId: entry.userId } : {}
180
202
  };
181
203
  writeProfiles(path, file);
182
204
  };
183
- /**
184
- * True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors
185
- * lazy creation: a single-account install has no profiles file, before or after.
186
- */
187
205
  const onlyDefaultRemains = (file) => {
188
206
  const names = Object.keys(file.profiles);
189
207
  return names.length === 0 || names.length === 1 && names[0] === "DEFAULT";
190
208
  };
209
+ const canDropProfilesFile = (file) => {
210
+ if (!onlyDefaultRemains(file)) return false;
211
+ const remaining = file.profiles[DEFAULT_PROFILE];
212
+ return remaining === void 0 || !isKeyringPointer(remaining.credentials);
213
+ };
214
+ const locationForName = (dir, name) => locationOf(resolveProfile(dir, name));
215
+ const newProfileLocation = (dir, name, storage) => storage === "keyring" ? {
216
+ profile: name,
217
+ storage: CRED_STORAGE_KEYRING
218
+ } : {
219
+ profile: name,
220
+ storage: CRED_STORAGE_FILE,
221
+ path: newProfileCredentialsPath(dir, name)
222
+ };
223
+ const profilesUsingPath = (dir, path, except) => {
224
+ const resolved = resolve(path);
225
+ return listProfiles(dir).filter((profile) => profile.name !== except && profile.storage === "file" && resolve(profile.credentialsPath) === resolved).map((profile) => profile.name);
226
+ };
191
227
  const listProfiles = (dir) => {
192
228
  const read = inspectProfiles(dir);
193
229
  if (read.kind === "unusable") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
@@ -203,10 +239,14 @@ const writeProfiles = (path, file) => {
203
239
  const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
204
240
  /** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
205
241
  const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
206
- /** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */
207
- const relativeToProfiles = (profilesPath, target) => {
208
- const rel = relative(resolve(profilesPath, ".."), target);
209
- return rel && !isAbsolute(rel) ? rel : target;
242
+ /** A relative file named `keyring` would otherwise collide with the storage sentinel. */
243
+ const storedPointer = (profilesPath, credentials) => {
244
+ if (isKeyringPointer(credentials)) return KEYRING_CREDENTIALS;
245
+ const base = resolve(profilesPath, "..");
246
+ const abs = isAbsolute(credentials) ? credentials : resolve(base, credentials);
247
+ const rel = relative(base, abs);
248
+ const stored = rel && !isAbsolute(rel) ? rel : abs;
249
+ return stored === "keyring" ? `./${KEYRING_CREDENTIALS}` : stored;
210
250
  };
211
251
  function nonEmpty(value) {
212
252
  if (typeof value !== "string") return void 0;
@@ -214,4 +254,4 @@ function nonEmpty(value) {
214
254
  return trimmed === "" ? void 0 : trimmed;
215
255
  }
216
256
  //#endregion
217
- export { newProfileCredentialsPath as a, readProfiles as c, upsertProfile as d, listProfiles as i, resolveProfile as l, assertProfilesUsable as n, onlyDefaultRemains as o, assertValidProfileName as r, profilesFilePath as s, DEFAULT_PROFILE as t, selectProfileName as u };
257
+ export { upsertProfile as _, canDropProfilesFile as a, writeSecretFile as b, listProfiles as c, newProfileLocation as d, profilesFilePath as f, selectProfileName as g, resolveProfile as h, assertValidProfileName as i, locationForName as l, readProfiles as m, KEYRING_CREDENTIALS as n, credentialsDisplay as o, profilesUsingPath as p, assertProfilesUsable as r, isKeyringPointer as s, DEFAULT_PROFILE as t, locationOf as u, CRED_STORAGE_FILE as v, CRED_STORAGE_KEYRING as y };
@@ -284,13 +284,14 @@ const parseStartupArgs = (argv) => {
284
284
  const applyShort = (letter, attachedValue, flagDisplay) => {
285
285
  const takesArg = SHORT_WITH_ARG.has(letter);
286
286
  let value;
287
- if (takesArg) if (attachedValue !== void 0) value = attachedValue;
288
- else {
289
- const v = needValue(flagDisplay);
290
- if (typeof v !== "string") return v;
291
- value = v;
292
- }
293
- else if (attachedValue !== void 0) return {
287
+ if (takesArg) {
288
+ if (attachedValue !== void 0) value = attachedValue;
289
+ else {
290
+ const v = needValue(flagDisplay);
291
+ if (typeof v !== "string") return v;
292
+ value = v;
293
+ }
294
+ } else if (attachedValue !== void 0) return {
294
295
  kind: "invalid-option",
295
296
  message: `option does not take an argument -- ${letter}`
296
297
  };
@@ -1,3 +1,4 @@
1
+ import "node:module";
1
2
  //#region \0rolldown/runtime.js
2
3
  var __defProp = Object.defineProperty;
3
4
  var __exportAll = (all, no_symbols) => {
package/dist/analytics.js CHANGED
@@ -1,9 +1,9 @@
1
- import { n as OAUTH, o as inspectCredentials } from "./_chunks/credentials-MYdHdKah.js";
1
+ import { a as OAUTH, t as storeFor } from "./_chunks/credential_io-D2vw1UOw.js";
2
2
  import { getGithubEnvVars, isCi } from "./env.js";
3
3
  import { log } from "./log.js";
4
4
  import pkg_default from "./pkg.js";
5
5
  import { getApiClient, isNeonApiError } from "./api.js";
6
- import { n as credentialsPath } from "./_chunks/paths-DMq0Lt7a.js";
6
+ import { r as credentialsPath } from "./_chunks/paths-g3DRqJiD.js";
7
7
  import "./config.js";
8
8
  import { getAuthContext } from "./auth_context.js";
9
9
  import { isCurrentBranchProbe } from "./context.js";
@@ -67,6 +67,7 @@ const storedCredentialAttribution = (storedUserId) => storedUserId ? {
67
67
  const telemetryCredential = (authContext, apiKey, defaultCredentialsPath) => {
68
68
  if (authContext === null) return { credentialsPath: defaultCredentialsPath };
69
69
  if (authContext.source === "api-key") return { apiKey };
70
+ if (authContext.storage === "keyring") return { apiKey };
70
71
  return {
71
72
  apiKey,
72
73
  credentialsPath: authContext.credentialsPath ?? defaultCredentialsPath
@@ -102,12 +103,25 @@ const analyticsMiddleware = async (args) => {
102
103
  if (isCurrentBranchProbe(args)) return;
103
104
  const { apiKey: keyToQuery, credentialsPath: fileToRead } = telemetryCredential(getAuthContext(), args.apiKey, credentialsPath(args.configDir));
104
105
  if (fileToRead !== void 0) try {
105
- const read = inspectCredentials(fileToRead);
106
- if (read.kind === "ok" && typeof read.credentials.user_id === "string") userId = read.credentials.user_id;
107
- else if (read.kind !== "ok") log.debug("No usable credentials at %s", fileToRead);
106
+ const listing = storeFor(args.configDir).inspect({
107
+ profile: getAuthContext()?.profile ?? "DEFAULT",
108
+ storage: "file",
109
+ path: fileToRead
110
+ });
111
+ if (typeof listing.credentials?.user_id === "string") userId = listing.credentials.user_id;
112
+ else log.debug("No usable credentials at %s", fileToRead);
108
113
  } catch (err) {
109
114
  log.debug("Could not read %s: %s", fileToRead, err);
110
115
  }
116
+ else if (getAuthContext()?.storage === "keyring") try {
117
+ const listing = storeFor(args.configDir).inspect({
118
+ profile: getAuthContext()?.profile ?? "DEFAULT",
119
+ storage: "keyring"
120
+ });
121
+ if (typeof listing.credentials?.user_id === "string") userId = listing.credentials.user_id;
122
+ } catch (err) {
123
+ log.debug("Could not read the OS keyring item: %s", err);
124
+ }
111
125
  try {
112
126
  if (keyToQuery) {
113
127
  const apiClient = getApiClient({
@@ -1,6 +1,19 @@
1
- import { a as isOwnedCredentialPath } from "./_chunks/paths-DMq0Lt7a.js";
1
+ import { o as isOwnedCredentialPath } from "./_chunks/paths-g3DRqJiD.js";
2
2
  import "./config.js";
3
3
  //#region src/auth_context.ts
4
+ const locationFromContext = (context) => {
5
+ if (context.profile === void 0) return null;
6
+ if (context.storage === "keyring") return {
7
+ profile: context.profile,
8
+ storage: "keyring"
9
+ };
10
+ if (context.credentialsPath === void 0) return null;
11
+ return {
12
+ profile: context.profile,
13
+ storage: "file",
14
+ path: context.credentialsPath
15
+ };
16
+ };
4
17
  let current = null;
5
18
  const setAuthContext = (context) => {
6
19
  current = context;
@@ -10,27 +23,12 @@ const getAuthContext = () => current;
10
23
  const clearAuthContext = () => {
11
24
  current = null;
12
25
  };
13
- /**
14
- * The credentials file a 401 should delete, or `null` to leave everything on disk.
15
- *
16
- * Only an expired OAuth token set is worth clearing: deleting it makes the next command log
17
- * in again, which is the recovery. Neither key-shaped source is.
18
- *
19
- * A key passed on the command line was never ours to store, so a 401 on it says nothing about
20
- * any stored credential — clearing one would sign the user out of an account the failed
21
- * request never used.
22
- *
23
- * A key read from a profile must survive for a sharper reason: unlike an OAuth token there is
24
- * nothing to refresh and no automatic way back, so deleting it would destroy the only copy of
25
- * a credential the user has to paste or mint again. It is also the expected state during
26
- * rotation, where the whole point is that the old key is dead and the file must still be there
27
- * to be replaced.
28
- */
29
26
  const credentialsToClearOn401 = (context) => {
30
27
  if (context?.source !== "stored-credentials") return null;
31
- const path = context.credentialsPath;
32
- if (path === void 0) return null;
33
- return isOwnedCredentialPath(context.configDir, path) ? path : null;
28
+ const at = locationFromContext(context);
29
+ if (at === null) return null;
30
+ if (at.storage === "keyring") return null;
31
+ return isOwnedCredentialPath(context.configDir, at.path) ? at : null;
34
32
  };
35
33
  /**
36
34
  * What to tell the user when the API rejects their credential, naming the profile and file
@@ -38,10 +36,13 @@ const credentialsToClearOn401 = (context) => {
38
36
  */
39
37
  const authFailureMessage = (context) => {
40
38
  const profile = context?.profile ?? "the selected profile";
41
- const where = context?.credentialsPath !== void 0 ? ` (${context.credentialsPath})` : "";
42
- if (context?.source === "profile-api-key") return `Authentication failed: the Neon API rejected profile "${profile}"'s API key${where}. Replace it with \`neon profile create ${profile} --mint --force\`, or store another with \`neon profile create ${profile} --api-key - --force\`.`;
43
- if (context?.source === "stored-credentials") return `Authentication failed: the Neon API rejected profile "${profile}"'s stored session${where}. That file was not created by neon, so it was left alone — sign in again with \`neon auth --profile ${profile}\`.`;
39
+ const where = context?.storage === "keyring" ? " (OS keyring)" : context?.credentialsPath !== void 0 ? ` (${context.credentialsPath})` : "";
40
+ if (context?.source === "profile-api-key") return `Authentication failed: the Neon API rejected profile "${profile}"'s API key${where}. Replace it with \`neon profile create ${profile} --mint\`, or store another with \`neon profile create ${profile} --api-key -\`.`;
41
+ if (context?.source === "stored-credentials") {
42
+ if (context.storage === "keyring") return `Authentication failed: the Neon API rejected profile "${profile}"'s stored session (OS keyring). Sign in again with \`neon auth --profile ${profile}\`.`;
43
+ return `Authentication failed: the Neon API rejected profile "${profile}"'s stored session${where}. That file was not created by neon, so it was left alone — sign in again with \`neon auth --profile ${profile}\`.`;
44
+ }
44
45
  return "Authentication failed: the Neon API rejected the API key. Check --api-key or NEON_API_KEY.";
45
46
  };
46
47
  //#endregion
47
- export { authFailureMessage, clearAuthContext, credentialsToClearOn401, getAuthContext, setAuthContext };
48
+ export { authFailureMessage, clearAuthContext, credentialsToClearOn401, getAuthContext, locationFromContext, setAuthContext };
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.js";
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { getEndpoints, loadSpec } from "../utils/openapi.js";
3
3
  import { writer } from "../writer.js";
4
4
  import { readFileSync } from "node:fs";
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.js";
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { isNeonApiError } from "../api.js";
4
4
  import { writer } from "../writer.js";