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,16 +1,15 @@
1
- import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.js";
2
- import { t as writeSecretFile } from "../_chunks/secure_file-BucZj4yQ.js";
3
- import { a as describeScope, c as isSameCredential, d as writeCredentials, i as credentialKind, l as readCredentials, n as OAUTH, o as inspectCredentials, r as apiKeyCredentials, s as interpretCredentials, t as API_KEY, u as scopeOf } from "../_chunks/credentials-MYdHdKah.js";
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { _ as upsertProfile, a as canDropProfilesFile, b as writeSecretFile, c as listProfiles, d as newProfileLocation, f as profilesFilePath, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, o as credentialsDisplay, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, u as locationOf, v as CRED_STORAGE_FILE, y as CRED_STORAGE_KEYRING } from "../_chunks/profiles-CVXNvU2r.js";
3
+ import { c as describeScope, d as scopeOf, l as interpretCredentials, n as KEYRING_SERVICE, o as apiKeyCredentials, r as keyringAccount, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-D2vw1UOw.js";
4
4
  import { isCi } from "../env.js";
5
5
  import { log } from "../log.js";
6
6
  import { getApiClient, isNeonApiError } from "../api.js";
7
- import { i as isInsideConfigDir } from "../_chunks/paths-DMq0Lt7a.js";
8
- import "../config.js";
7
+ import { o as isOwnedCredentialPath, r as credentialsPath } from "../_chunks/paths-g3DRqJiD.js";
9
8
  import { setAuthContext } from "../auth_context.js";
10
9
  import { auth, revokeToken } from "../auth.js";
11
- import { a as newProfileCredentialsPath, c as readProfiles, d as upsertProfile, i as listProfiles, l as resolveProfile, n as assertProfilesUsable, o as onlyDefaultRemains, r as assertValidProfileName, s as profilesFilePath, u as selectProfileName } from "../_chunks/profiles-Ir29rqns.js";
12
- import { t as credentialInputs } from "../_chunks/auth_selection-DGgq6ifc.js";
13
- import { authFlow, credentialsPathForName, usableCredential } from "./auth.js";
10
+ import { t as credentialInputs } from "../_chunks/auth_selection-CV_3n1du.js";
11
+ import { readOutgoingCredential, retirePreviousCredential, revokeTokenSet, withdrawKey } from "../retire_credential.js";
12
+ import { authFlow, usableCredential } from "./auth.js";
14
13
  import { writer } from "../writer.js";
15
14
  import { noPassthrough, single } from "../utils/flags.js";
16
15
  import { orgIdForProject } from "./api_keys.js";
@@ -55,12 +54,22 @@ const builder = (argv) => argv.usage("$0 profile <sub-command> [options]").comma
55
54
  type: "string",
56
55
  coerce: single("project-id")
57
56
  },
57
+ keyring: {
58
+ describe: "Store the credential in the OS keyring. Per profile; later create without the flag stays there. See `neon profile list`.",
59
+ type: "boolean"
60
+ },
58
61
  force: {
59
- describe: "Replace an existing profile, revoking the credential it holds now",
60
- type: "boolean",
61
- default: false
62
+ hidden: true,
63
+ type: "boolean"
62
64
  }
63
- }).conflicts("org-id", "project-id").strict().check(noPassthrough("profile create")).example("$0 profile create work", "Sign in with the browser, like `neon auth --profile work`").example("$0 profile create work --api-key \"$KEY\"", "Store a key you already have").example("echo \"$KEY\" | $0 profile create work --api-key -", "Or pipe it, so it never reaches the process arguments").example("$0 profile create ci --mint --org-id org-abc-123", "One browser sign-in, then an org-scoped key and no session left behind"), async (args) => await create(args)).command("rotate-key <name>", "Mint a fresh API key for a profile, at the same scope, and revoke the one it replaces", (y) => y.positional("name", {
65
+ }).conflicts("org-id", "project-id").strict().check((argv) => {
66
+ noPassthrough("profile create")(argv);
67
+ if (argv.force === true) {
68
+ const name = typeof argv.name === "string" ? argv.name : "NAME";
69
+ throw new Error(`--force is no longer a flag. \`neon profile create ${name}\` always replaces an existing profile and revokes the credential it held. Drop --force.`);
70
+ }
71
+ return true;
72
+ }).example("$0 profile create work", "Sign in with the browser, like `neon auth --profile work`").example("$0 profile create work --api-key \"$KEY\"", "Store a key you already have").example("echo \"$KEY\" | $0 profile create work --api-key -", "Or pipe it, so it never reaches the process arguments").example("$0 profile create ci --mint --org-id org-abc-123", "One browser sign-in, then an org-scoped key and no session left behind").example("$0 profile create work --keyring", "Store the credential in the OS keyring instead of a file"), async (args) => await create(args)).command("rotate-key <name>", "Mint a fresh API key for a profile, at the same scope, and revoke the one it replaces", (y) => y.positional("name", {
64
73
  describe: "Profile to rotate",
65
74
  type: "string",
66
75
  demandOption: true
@@ -114,10 +123,11 @@ const rejectApiKeyFlag = (sub, instead) => {
114
123
  * rather than throwing: one broken profile must not hide every other row, and the whole point
115
124
  * of `list` is to show the state of things including the broken parts.
116
125
  */
117
- const describeAuth = (stored, at) => {
126
+ const hintStore = (storage) => storage === "keyring" ? "keyring" : "file";
127
+ const describeAuth = (stored, at, store = "file") => {
118
128
  if (stored === null) return "-";
119
129
  try {
120
- return interpretCredentials(stored, at).kind === "api_key" ? "api key" : "oauth";
130
+ return interpretCredentials(stored, at, store).kind === "api_key" ? "api key" : "oauth";
121
131
  } catch (err) {
122
132
  log.warning(err instanceof Error ? err.message : String(err));
123
133
  return "invalid";
@@ -134,26 +144,21 @@ const describeAuth = (stored, at) => {
134
144
  * unreadable used to show `File: ok` beside `Auth: invalid`, so the column a user scans to find
135
145
  * the broken profile pointed at a different row than the one that was broken.
136
146
  */
137
- const inspectForListing = (at) => {
138
- const read = inspectCredentials(at.path);
139
- if (read.kind === "unusable") {
140
- log.warning(read.reason);
141
- return {
142
- stored: null,
143
- auth: "-",
144
- file: "invalid"
145
- };
146
- }
147
- if (read.kind === "absent") return {
147
+ const inspectForListing = (configDir, at) => {
148
+ const listing = storeFor(configDir).inspect(at);
149
+ if (listing.reason !== void 0) log.warning(listing.reason);
150
+ if (listing.credentials === null) return {
148
151
  stored: null,
149
152
  auth: "-",
150
- file: "missing"
153
+ file: listing.file,
154
+ storage: listing.storage
151
155
  };
152
- const auth = describeAuth(read.credentials, at);
156
+ const auth = describeAuth(listing.credentials, at, hintStore(listing.storage));
153
157
  return {
154
- stored: read.credentials,
158
+ stored: listing.credentials,
155
159
  auth,
156
- file: auth === "invalid" ? "invalid" : "ok"
160
+ file: auth === "invalid" ? "invalid" : listing.file,
161
+ storage: listing.storage
157
162
  };
158
163
  };
159
164
  const list = async (props) => {
@@ -161,10 +166,7 @@ const list = async (props) => {
161
166
  const active = selectProfileName(props.profile);
162
167
  if (props.profile !== void 0 && props.profile.trim() !== "") resolveProfile(props.configDir, active);
163
168
  const rows = listProfiles(props.configDir).map((p) => {
164
- const { stored, auth, file } = inspectForListing({
165
- path: p.credentialsPath,
166
- profile: p.name
167
- });
169
+ const { stored, auth, file, storage } = inspectForListing(props.configDir, locationOf(p));
168
170
  const storedUserId = typeof stored?.user_id === "string" ? stored.user_id : void 0;
169
171
  return {
170
172
  active: p.name === active ? "*" : "",
@@ -173,7 +175,8 @@ const list = async (props) => {
173
175
  auth,
174
176
  scope: stored !== null && auth === "api key" ? describeScope(scopeOf(stored)) : "-",
175
177
  file,
176
- ...props.output === "table" ? { credentials: basename(p.credentialsPath) } : { credentials: p.credentialsPath }
178
+ storage,
179
+ ...props.output === "table" ? { credentials: p.storage === "keyring" ? KEYRING_CREDENTIALS : basename(p.credentialsPath) } : { credentials: credentialsDisplay(p) }
177
180
  };
178
181
  });
179
182
  writer(props).end(rows, {
@@ -185,71 +188,46 @@ const list = async (props) => {
185
188
  "auth",
186
189
  "scope",
187
190
  "file",
191
+ "storage",
188
192
  "credentials"
189
193
  ]
190
194
  });
191
195
  };
192
- /**
193
- * The credential a command is about to replace or delete.
194
- *
195
- * Deliberately tolerant where {@link readCredentials} is fatal. Using a damaged credential must
196
- * fail loudly, but *replacing* one must not: `readCredentials` throwing here made the repair its
197
- * own error message recommends impossible, and left a malformed file unremovable through the
198
- * CLI. There is nothing to revoke in a file we cannot parse, so this says so and moves on.
199
- */
200
- const readOutgoingCredential = (at) => {
201
- const read = inspectCredentials(at.path);
202
- const unusable = (reason) => {
203
- log.warning("%s. Nothing in it could be revoked, so it is only being replaced locally.", reason.replace(/\.$/, ""));
204
- return null;
196
+ const locationForCreate = (configDir, name, keyring) => {
197
+ assertProfilesUsable(configDir, name);
198
+ const declared = readProfiles(configDir)?.profiles[name];
199
+ const pointer = declared !== void 0 && isKeyringPointer(declared.credentials);
200
+ if (keyring === true || pointer) return {
201
+ profile: name,
202
+ storage: CRED_STORAGE_KEYRING
205
203
  };
206
- if (read.kind === "unusable") return unusable(read.reason);
207
- if (read.kind === "absent") return null;
204
+ if (declared !== void 0 || name === "DEFAULT") return locationForName(configDir, name);
205
+ return newProfileLocation(configDir, name, CRED_STORAGE_FILE);
206
+ };
207
+ const existingLocation = (configDir, name) => {
208
208
  try {
209
- const credential = interpretCredentials(read.credentials, at);
210
- if (credential.kind === "oauth") return {
211
- kind: OAUTH,
212
- tokens: read.credentials
213
- };
214
- return {
215
- kind: API_KEY,
216
- apiKey: credential.apiKey,
217
- ...typeof read.credentials.key_id === "number" ? { keyId: read.credentials.key_id } : {},
218
- scope: scopeOf(read.credentials)
219
- };
220
- } catch (err) {
221
- return unusable(err instanceof Error ? err.message : String(err));
209
+ if (readProfiles(configDir)?.profiles[name] !== void 0 || name === "DEFAULT") return locationForName(configDir, name);
210
+ } catch {
211
+ return null;
222
212
  }
213
+ return null;
223
214
  };
224
- const credentialsPathFor = (configDir, name) => {
225
- assertProfilesUsable(configDir, name);
226
- if (readProfiles(configDir)?.profiles[name] || name === "DEFAULT") return credentialsPathForName(configDir, name);
227
- return newProfileCredentialsPath(configDir, name);
215
+ const warnIfKeyringRollbackUnconfirmed = (cleared, profile) => {
216
+ if (cleared === "cleared") return;
217
+ log.warning("Could not confirm the new OS keyring item for profile \"%s\" was removed after a failed save.", profile);
228
218
  };
229
- /**
230
- * Refuse to overwrite an existing profile unless asked to, and say what `--force` destroys.
231
- *
232
- * Naming the consequence is the point. `--force` does not merely point the profile somewhere
233
- * else: {@link retirePreviousCredential} revokes the credential being replaced, so a key this
234
- * CLI minted dies upstream — including the copy the user pasted into CI or another machine,
235
- * which is not recoverable, only re-mintable. A message promising "replace its credential"
236
- * described a local edit and made the irreversible half a surprise.
237
- */
238
- const assertReplaceable = (props) => {
239
- const { name, configDir, force } = props;
240
- if (force) return;
241
- const declared = readProfiles(configDir)?.profiles[name];
242
- const path = credentialsPathFor(configDir, name);
243
- if (!declared && !(name === "DEFAULT" && existsSync(path))) return;
244
- const existing = inspectCredentials(path);
245
- const stored = existing.kind === "ok" ? existing.credentials : null;
246
- const holdsKey = stored !== null && credentialKind(stored, {
247
- path,
248
- profile: name
249
- }) === "api_key";
250
- const keyId = typeof stored?.key_id === "number" ? stored.key_id : void 0;
251
- if (holdsKey) throw new Error(keyId !== void 0 ? `Profile "${name}" already exists and holds an API key minted here (id ${keyId}). Pass --force to replace it — the key is revoked, wherever else it is in use. To keep the profile and swap the key instead: \`neon profile rotate-key ${name}\`.` : `Profile "${name}" already exists and holds an API key you supplied, which stays live on the account either way — only keys minted here record an id to revoke. Pass --force to replace it locally, or \`neon profile rotate-key ${name}\` to mint one at the same scope.`);
252
- throw new Error(`Profile "${name}" already exists and holds a browser sign-in. Pass --force to replace it — the session is signed out as part of the replacement.`);
219
+ const deleteLeftoverOwnedFile = (configDir, path, except) => {
220
+ if (!isOwnedCredentialPath(configDir, path)) return;
221
+ if (profilesUsingPath(configDir, path, except).length > 0) return;
222
+ try {
223
+ storeFor(configDir).delete({
224
+ profile: except,
225
+ storage: CRED_STORAGE_FILE,
226
+ path
227
+ });
228
+ } catch {
229
+ log.warning("Saved the keyring item but could not delete %s", path);
230
+ }
253
231
  };
254
232
  /** `--api-key -` means "read it from stdin", the usual convention for a piped value. */
255
233
  const STDIN = "-";
@@ -314,7 +292,7 @@ const create = async (props) => {
314
292
  rejectProfileFlag(props, "create");
315
293
  assertValidProfileName(name);
316
294
  assertProfilesUsable(props.configDir, name);
317
- assertReplaceable(props);
295
+ if (props.keyring === true) storeFor(props.configDir).assertKeyringWritable(existingLocation(props.configDir, name)?.storage === "keyring" ? name : void 0);
318
296
  const suppliedKey = credentialInputs().apiKeyFlag.trim() !== "";
319
297
  if (props.mint) {
320
298
  if (suppliedKey) throw new Error("--mint creates a new key, so it cannot be combined with --api-key. Drop one.");
@@ -324,47 +302,53 @@ const create = async (props) => {
324
302
  if ((props.orgId ?? props.projectId) !== void 0) throw new Error("--org-id and --project-id only apply with --mint: they choose what a key we mint can reach, and a key you supply already has a scope.");
325
303
  if (!suppliedKey) {
326
304
  if (isCi() && props.forceAuth !== true) throw new Error(`\`neon profile create ${name}\` with no key signs in through the browser, which cannot happen in CI. Pass a key instead: \`neon profile create ${name} --api-key "$KEY"\`, or pipe it with \`echo "$KEY" | neon profile create ${name} --api-key -\`.`);
327
- const credentialsPath = credentialsPathFor(props.configDir, name);
328
- const previous = readOutgoingCredential({
329
- path: credentialsPath,
330
- profile: name
331
- });
305
+ const at = locationForCreate(props.configDir, name, props.keyring);
306
+ const existing = existingLocation(props.configDir, name);
307
+ const previous = existing === null ? null : readOutgoingCredential(props.configDir, existing);
332
308
  if (await authFlow({
333
309
  ...props,
334
310
  _: ["auth"],
335
- profile: name
311
+ profile: name,
312
+ keyring: props.keyring
336
313
  }) === "") throw new Error(`Could not save credentials for profile "${name}".`);
337
- await retirePreviousCredential(props, name, previous);
314
+ if (at.storage !== "keyring") await retirePreviousCredential(props, name, previous);
338
315
  const signedIn = readProfiles(props.configDir, log.warning)?.profiles[name];
339
316
  report(props, {
340
317
  name,
341
318
  account: signedIn?.label ?? signedIn?.userId ?? "unknown account",
342
319
  auth: "oauth",
343
320
  scope: "-",
344
- credentials: credentialsPath
321
+ credentials: at.storage === "keyring" ? KEYRING_CREDENTIALS : at.path,
322
+ storage: at.storage
345
323
  });
346
324
  return;
347
325
  }
348
326
  const apiKey = resolveKeyToStore(props);
349
327
  const identity = await verifyKey(props, apiKey);
350
- const credentialsPath = credentialsPathFor(props.configDir, name);
351
- const previous = readOutgoingCredential({
352
- path: credentialsPath,
353
- profile: name
354
- });
355
- writeCredentials(credentialsPath, apiKeyCredentials({
328
+ const at = locationForCreate(props.configDir, name, props.keyring);
329
+ const existing = existingLocation(props.configDir, name);
330
+ if (at.storage === "keyring") storeFor(props.configDir).assertKeyringWritable(existing?.storage === "keyring" ? name : void 0);
331
+ const previous = existing === null ? null : readOutgoingCredential(props.configDir, existing);
332
+ storeFor(props.configDir).write(at, apiKeyCredentials({
356
333
  apiKey,
357
334
  ...identity.userId !== void 0 ? { userId: identity.userId } : {},
358
335
  ...identity.orgId !== void 0 ? { scope: { orgId: identity.orgId } } : {}
359
336
  }));
337
+ try {
338
+ recordProfile(props, name, at, identity);
339
+ } catch (err) {
340
+ if (at.storage === "keyring" && existing?.storage !== "keyring") warnIfKeyringRollbackUnconfirmed(storeFor(props.configDir).delete(at, { required: false }), name);
341
+ throw err instanceof Error ? err : new Error(String(err));
342
+ }
360
343
  await retirePreviousCredential(props, name, previous, apiKey);
361
- recordProfile(props, name, credentialsPath, identity);
344
+ if (at.storage === "keyring" && existing?.storage === "file") deleteLeftoverOwnedFile(props.configDir, existing.path, name);
362
345
  report(props, {
363
346
  name,
364
347
  account: identity.label ?? "unknown account",
365
348
  auth: "api key",
366
349
  scope: describeScope(identity.orgId !== void 0 ? { orgId: identity.orgId } : {}),
367
- credentials: credentialsPath
350
+ credentials: at.storage === "keyring" ? KEYRING_CREDENTIALS : at.path,
351
+ storage: at.storage
368
352
  });
369
353
  log.warning("A key you supplied cannot be revoked by `rotate-key` or `remove` — only keys minted here record an id.");
370
354
  };
@@ -404,14 +388,15 @@ const report = (props, record) => {
404
388
  "auth",
405
389
  "scope",
406
390
  ...record.keyId !== void 0 ? ["keyId"] : [],
407
- "credentials"
391
+ "credentials",
392
+ ...record.storage !== void 0 ? ["storage"] : []
408
393
  ],
409
394
  title: "Profile"
410
395
  });
411
396
  };
412
- const recordProfile = (props, name, credentialsPath, identity) => {
397
+ const recordProfile = (props, name, at, identity) => {
413
398
  upsertProfile(props.configDir, name, {
414
- credentials: credentialsPath,
399
+ credentials: at.storage === "keyring" ? KEYRING_CREDENTIALS : at.path,
415
400
  ...identity.label ? { label: identity.label } : {},
416
401
  ...identity.userId ? { userId: identity.userId } : {}
417
402
  });
@@ -427,6 +412,7 @@ const recordProfile = (props, name, credentialsPath, identity) => {
427
412
  const createByMinting = async (props) => {
428
413
  const { name } = props;
429
414
  if (isCi() && props.forceAuth !== true) throw new Error(`--mint needs a browser sign-in, which cannot happen in CI. Mint the key with \`neon api-keys create --name ${name}\` and pipe it in: echo "$KEY" | neon profile create ${name} --api-key -`);
415
+ if (locationForCreate(props.configDir, name, props.keyring).storage === "keyring") storeFor(props.configDir).assertKeyringWritable(existingLocation(props.configDir, name)?.storage === "keyring" ? name : void 0);
430
416
  const oauthProps = {
431
417
  oauthHost: props.oauthHost,
432
418
  clientId: props.clientId,
@@ -445,27 +431,33 @@ const createByMinting = async (props) => {
445
431
  mintedScope = scope;
446
432
  minted = await mintKey(session, name, scope);
447
433
  const identity = await verifyKey(props, minted.key, "minted");
448
- const credentialsPath = credentialsPathFor(props.configDir, name);
449
- const previous = readOutgoingCredential({
450
- path: credentialsPath,
451
- profile: name
452
- });
453
- writeCredentials(credentialsPath, apiKeyCredentials({
434
+ const at = locationForCreate(props.configDir, name, props.keyring);
435
+ const existing = existingLocation(props.configDir, name);
436
+ const previous = existing === null ? null : readOutgoingCredential(props.configDir, existing);
437
+ storeFor(props.configDir).write(at, apiKeyCredentials({
454
438
  apiKey: minted.key,
455
439
  keyId: minted.id,
456
440
  ...identity.userId !== void 0 ? { userId: identity.userId } : {},
457
441
  scope
458
442
  }));
459
- keyIsReachable = true;
443
+ try {
444
+ recordProfile(props, name, at, identity);
445
+ } catch (err) {
446
+ if (at.storage === "keyring" && existing?.storage !== "keyring") warnIfKeyringRollbackUnconfirmed(storeFor(props.configDir).delete(at, { required: false }), name);
447
+ else keyIsReachable = true;
448
+ throw err instanceof Error ? err : new Error(String(err));
449
+ }
460
450
  await retirePreviousCredential(props, name, previous, minted.key);
461
- recordProfile(props, name, credentialsPath, identity);
451
+ if (at.storage === "keyring" && existing?.storage === "file") deleteLeftoverOwnedFile(props.configDir, existing.path, name);
452
+ keyIsReachable = true;
462
453
  report(props, {
463
454
  name,
464
455
  account: identity.label ?? "unknown account",
465
456
  auth: "api key",
466
457
  scope: describeScope(scope),
467
458
  keyId: minted.id,
468
- credentials: credentialsPath
459
+ credentials: at.storage === "keyring" ? KEYRING_CREDENTIALS : at.path,
460
+ storage: at.storage
469
461
  });
470
462
  warnAboutReach(scope);
471
463
  } finally {
@@ -489,36 +481,6 @@ const resolveMintScope = async (props, client) => {
489
481
  };
490
482
  };
491
483
  /**
492
- * Revoke a credential a profile has just stopped using.
493
- *
494
- * Called with the credential read *before* the overwrite, and only *after* the replacement is
495
- * durable. Both halves matter. Reading it first is the only chance: a minted key's `key_id` and
496
- * an OAuth refresh token are gone the moment the file is rewritten, so nothing could revoke
497
- * them afterwards. Revoking last is what stops a cancelled sign-in or a failed write from
498
- * leaving the profile holding a credential that has already been killed.
499
- */
500
- const retirePreviousCredential = async (props, name, existing, replacementKey) => {
501
- if (existing === null) return;
502
- if (existing.kind === "api_key") {
503
- if (isSameCredential(existing.apiKey, replacementKey)) {
504
- log.debug("The replacement is the credential already stored; nothing to retire.");
505
- return;
506
- }
507
- if (existing.keyId === void 0) {
508
- log.warning("Profile \"%s\" held an API key that was supplied rather than minted here, so it stays live on the account — find it with `neon api-keys list`.", name);
509
- return;
510
- }
511
- const client = getApiClient({
512
- apiKey: existing.apiKey,
513
- apiHost: props.apiHost
514
- });
515
- log.info(await withdrawKey(client, existing.scope, existing.keyId) ? `Revoked the key it replaces (id ${existing.keyId})` : `Could not revoke the key it replaces (id ${existing.keyId}); it may still be live. Remove it with: neon api-keys revoke ${existing.keyId}`);
516
- return;
517
- }
518
- const revoked = await revokeTokenSet(existing.tokens, props);
519
- log.info(revoked ? "Signed out the session it replaced" : "Could not sign out the session it replaced; it will expire on its own");
520
- };
521
- /**
522
484
  * Mint a key at the given scope and refuse anything that came back different.
523
485
  *
524
486
  * A 2xx is not enough. A response with no `key` leaves a live credential the user can never
@@ -541,30 +503,16 @@ const mintKey = async (client, profile, scope) => {
541
503
  const withdrawn = await withdrawKey(client, scope, data.id);
542
504
  throw new Error(`${problem} ${withdrawn ? "The key has been revoked; nothing was stored." : `The key could NOT be revoked and may still be live. Remove it with \`neon api-keys revoke ${data.id}${scope.orgId ? ` --org-id ${scope.orgId}` : ""}\`.`}`);
543
505
  };
544
- /** Best-effort withdrawal of a key we are refusing to store. Never throws. */
545
- const withdrawKey = async (client, scope, keyId) => {
546
- if (!Number.isSafeInteger(keyId) || keyId <= 0) return false;
547
- try {
548
- const { data } = scope.orgId ? await client.revokeOrgApiKey(scope.orgId, keyId) : await client.revokeApiKey(keyId);
549
- return data.revoked === true && data.id === keyId;
550
- } catch (err) {
551
- log.error("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
552
- return false;
553
- }
554
- };
555
506
  const rotateKey = async (props) => {
556
507
  const { name } = props;
557
508
  rejectProfileFlag(props, "rotate-key");
558
- rejectApiKeyFlag("rotate-key", `To store a key you already have, use \`neon profile create ${name} --api-key - --force\`.`);
509
+ rejectApiKeyFlag("rotate-key", `To store a key you already have, use \`neon profile create ${name} --api-key -\`.`);
559
510
  const profile = resolveProfile(props.configDir, name);
560
- const at = {
561
- path: profile.credentialsPath,
562
- profile: name
563
- };
511
+ const at = locationOf(profile);
564
512
  const credential = await usableCredential(props, at);
565
- if (credential === null) throw new Error(`Profile "${name}" has no usable credential to mint with. Replace it with \`neon profile create ${name} --mint --force\`.`);
566
- if (credential.kind !== "api_key") throw new Error(`Profile "${name}" holds a browser sign-in, not an API key, so there is no key to rotate. Turn it into a key profile with \`neon profile create ${name} --mint --force\`.`);
567
- const stored = readCredentials(at);
513
+ if (credential === null) throw new Error(`Profile "${name}" has no usable credential to mint with. Replace it with \`neon profile create ${name} --mint\`.`);
514
+ if (credential.kind !== "api_key") throw new Error(`Profile "${name}" holds a browser sign-in, not an API key, so there is no key to rotate. Turn it into a key profile with \`neon profile create ${name} --mint\`.`);
515
+ const stored = storeFor(props.configDir).read(at)?.credentials ?? null;
568
516
  const scope = stored !== null ? scopeOf(stored) : {};
569
517
  const previousKeyId = typeof stored?.key_id === "number" ? stored.key_id : void 0;
570
518
  const minting = getApiClient({
@@ -575,24 +523,25 @@ const rotateKey = async (props) => {
575
523
  source: "profile-api-key",
576
524
  configDir: props.configDir,
577
525
  profile: name,
578
- credentialsPath: profile.credentialsPath
526
+ storage: at.storage,
527
+ ...at.storage === "file" ? { credentialsPath: at.path } : {}
579
528
  });
580
529
  const { data: details } = await minting.getAuthDetails();
581
530
  if (details.auth_method === "api_key_org") {
582
- const advice = scope.projectId !== void 0 ? `\`neon profile create ${name} --mint --project-id ${scope.projectId} --force\`` : scope.orgId !== void 0 && previousKeyId !== void 0 ? `\`neon profile create ${name} --mint --org-id ${scope.orgId} --force\`` : `\`neon profile create ${name} --mint --org-id ${details.account_id} --force\` — but check \`neon api-keys list --org-id ${details.account_id}\` first, because a key you supplied may have been narrowed to a single project and an organization key would reach more`;
531
+ const advice = scope.projectId !== void 0 ? `\`neon profile create ${name} --mint --project-id ${scope.projectId}\`` : scope.orgId !== void 0 && previousKeyId !== void 0 ? `\`neon profile create ${name} --mint --org-id ${scope.orgId}\`` : `\`neon profile create ${name} --mint --org-id ${details.account_id}\` — but check \`neon api-keys list --org-id ${details.account_id}\` first, because a key you supplied may have been narrowed to a single project and an organization key would reach more`;
583
532
  throw new Error(`Profile "${name}" holds an organization key, and only a personal credential can mint organization keys — so it cannot mint its own replacement. Sign in and mint one with ${advice}.`);
584
533
  }
585
534
  const created = await mintKey(minting, name, scope);
586
535
  log.info("Minted %s (id %d, %s)", created.name, created.id, describeScope(scope));
587
536
  try {
588
- writeCredentials(profile.credentialsPath, apiKeyCredentials({
537
+ storeFor(props.configDir).write(at, apiKeyCredentials({
589
538
  apiKey: created.key,
590
539
  keyId: created.id,
591
540
  ...typeof stored?.user_id === "string" ? { userId: stored.user_id } : {},
592
541
  scope
593
542
  }));
594
543
  } catch (err) {
595
- log.error("Minted key %d but could not write %s. Revoke it with: neon api-keys revoke %d%s", created.id, profile.credentialsPath, created.id, scope.orgId ? ` --org-id ${scope.orgId}` : "");
544
+ log.error("Minted key %d but could not write %s. Revoke it with: neon api-keys revoke %d%s", created.id, credentialLabel(at), created.id, scope.orgId ? ` --org-id ${scope.orgId}` : "");
596
545
  throw err;
597
546
  }
598
547
  report(props, {
@@ -601,7 +550,8 @@ const rotateKey = async (props) => {
601
550
  auth: "api key",
602
551
  scope: describeScope(scope),
603
552
  keyId: created.id,
604
- credentials: profile.credentialsPath
553
+ credentials: credentialsDisplay(profile),
554
+ storage: at.storage
605
555
  });
606
556
  if (previousKeyId === void 0) {
607
557
  log.warning("No previous key id was recorded, so the key this replaces is still live. It was supplied rather than minted here, and `GET /api_keys` exposes no prefix to match it by — find it with `neon api-keys list`.");
@@ -633,11 +583,14 @@ const remove = async (props) => {
633
583
  });
634
584
  if (!ok) throw new Error(`Cancelled — profile "${name}" was not removed.`);
635
585
  }
636
- const at = {
637
- path: profile.credentialsPath,
638
- profile: name
639
- };
640
- const stored = readOutgoingCredential(at);
586
+ const at = locationOf(profile);
587
+ let stored = null;
588
+ try {
589
+ stored = readOutgoingCredential(props.configDir, at);
590
+ } catch (err) {
591
+ if (at.storage === "keyring") log.warning("Could not read the OS keyring item for profile \"%s\"; nothing in it could be revoked.", name);
592
+ else log.warning("%s. Nothing in it could be revoked.", (err instanceof Error ? err.message : String(err)).replace(/\.$/, ""));
593
+ }
641
594
  if (stored?.kind === "api_key") {
642
595
  const { keyId, scope } = stored;
643
596
  if (keyId === void 0) log.warning("Profile \"%s\" holds an API key that was supplied rather than minted here, so it stays live on the account — find it with `neon api-keys list`.", name);
@@ -652,31 +605,59 @@ const remove = async (props) => {
652
605
  const revoked = await revokeTokenSet(stored.tokens, props);
653
606
  log.info(revoked ? "Revoked the OAuth token" : "Could not revoke the OAuth token — removing locally anyway");
654
607
  }
655
- if (existsSync(profile.credentialsPath)) if (isInsideConfigDir(props.configDir, profile.credentialsPath)) {
656
- rmSync(profile.credentialsPath);
657
- log.info("Deleted %s", profile.credentialsPath);
658
- } else log.info("Left %s on disk — not created by neon", profile.credentialsPath);
659
- const path = profilesFilePath(props.configDir);
660
- const file = readProfiles(props.configDir, log.warning);
661
- if (file?.profiles[name]) {
662
- delete file.profiles[name];
663
- if (onlyDefaultRemains(file)) {
664
- rmSync(path);
665
- log.info("Removed \"%s\" — no profiles left, deleted %s", name, path);
666
- } else {
667
- writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
668
- log.info("Removed \"%s\" from %s", name, path);
608
+ const dropPointer = () => {
609
+ assertProfilesUsable(props.configDir, name);
610
+ const path = profilesFilePath(props.configDir);
611
+ const file = readProfiles(props.configDir, log.warning);
612
+ if (file?.profiles[name]) {
613
+ delete file.profiles[name];
614
+ if (canDropProfilesFile(file)) {
615
+ rmSync(path);
616
+ log.info("Removed \"%s\" — no profiles left, deleted %s", name, path);
617
+ } else {
618
+ writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
619
+ log.info("Removed \"%s\" from %s", name, path);
620
+ }
621
+ } else if (name === "DEFAULT") log.info("Signed out of DEFAULT");
622
+ };
623
+ const listing = storeFor(props.configDir).inspect(at);
624
+ if (at.storage === "keyring") {
625
+ const account = keyringAccount(props.configDir, name);
626
+ if (name === "DEFAULT") {
627
+ const leftover = credentialsPath(props.configDir);
628
+ if (isOwnedCredentialPath(props.configDir, leftover) && existsSync(leftover) && profilesUsingPath(props.configDir, leftover, name).length === 0) {
629
+ storeFor(props.configDir).delete({
630
+ profile: name,
631
+ storage: CRED_STORAGE_FILE,
632
+ path: leftover
633
+ }, { required: false });
634
+ if (existsSync(leftover)) throw new Error(`Could not delete ${leftover}. Refusing to reset DEFAULT — that file would become the profile.`);
635
+ log.info("Deleted %s", leftover);
636
+ }
669
637
  }
670
- } else if (name === "DEFAULT") log.info("Signed out of DEFAULT");
671
- };
672
- /** Revoke the OAuth refresh token in a credential we already have in hand. */
673
- const revokeTokenSet = async (credentials, props) => {
674
- const tokenSet = credentials;
675
- return await revokeToken({
676
- oauthHost: props.oauthHost,
677
- clientId: props.clientId,
678
- ...props.allowUnsafeTls ? { allowUnsafeTls: props.allowUnsafeTls } : {}
679
- }, tokenSet);
638
+ dropPointer();
639
+ if (listing.reason?.includes("cannot use the OS keyring")) {
640
+ log.warning("This CLI cannot delete the OS keyring item for profile \"%s\". It remains in the OS store (service %s, account %s) until a CLI with the keyring addon removes it, or you delete it there.", name, KEYRING_SERVICE, account);
641
+ return;
642
+ }
643
+ let cleared = "unconfirmed";
644
+ try {
645
+ cleared = storeFor(props.configDir).delete(at, {
646
+ required: false,
647
+ account
648
+ });
649
+ } catch {
650
+ cleared = "unconfirmed";
651
+ }
652
+ if (cleared === "cleared") log.info("Deleted the OS keyring item for profile \"%s\"", name);
653
+ else log.warning("Could not confirm the OS keyring item for profile \"%s\" is gone. A leftover may still be in the OS store (service %s, account %s); it is not used once the profile is gone.", name, KEYRING_SERVICE, account);
654
+ return;
655
+ }
656
+ if (isOwnedCredentialPath(props.configDir, at.path)) {
657
+ storeFor(props.configDir).delete(at);
658
+ if (listing.credentials !== null || listing.file !== "missing") log.info("Deleted %s", at.path);
659
+ } else if (existsSync(at.path)) log.info("Left %s on disk — not created by neon", at.path);
660
+ dropPointer();
680
661
  };
681
662
  //#endregion
682
663
  export { aliases, builder, command, describe, handler, profile_exports as t };
@@ -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 { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { isNeonApiError, messageFromBody } from "../api.js";
@@ -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 { EndpointType } from "../utils/api_enums.js";
3
3
  import { fillSingleProject } from "../utils/enrichers.js";
4
4
  import { SSL_MODES, handler as handler$1 } from "./connection_string.js";
@@ -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 { retryOnLock } from "../api.js";
3
3
  import { writer } from "../writer.js";
4
4
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";