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
package/README.md CHANGED
@@ -712,7 +712,7 @@ $ echo $?
712
712
 
713
713
  That message reports where parsing stopped and nothing more. `--data` carries whatever you put in it, and the JSON parser's own message quotes a window of the input, so echoing either would put a connection string or an API key on stdout.
714
714
 
715
- **One exception to "JSON on stdout".** Credentials are resolved before any command runs, so a failure in that step — an unknown `--profile` or `NEON_PROFILE`, `--api-key` and `--profile` together, or a `credentials.json` that cannot be read — prints `ERROR: …` on stderr, leaves stdout empty, and exits 1. Treat a non-zero exit with empty stdout as a credential problem and read stderr.
715
+ **One exception to "JSON on stdout".** Credentials are resolved before any command runs, so a failure in that step — an unknown `--profile` or `NEON_PROFILE`, `--api-key` and `--profile` together, a `credentials.json` that cannot be read, or an OS keyring item that cannot be read — prints `ERROR: …` on stderr, leaves stdout empty, and exits 1. Treat a non-zero exit with empty stdout as a credential problem and read stderr.
716
716
 
717
717
  | Option | |
718
718
  | --- | --- |
@@ -822,17 +822,19 @@ Two combinations are rejected before the request: `--since` with `--start-time`,
822
822
 
823
823
  ## Profiles
824
824
 
825
- The CLI holds one Neon account by default. A profile adds another, and is nothing more than a pointer to a credentials file:
825
+ The CLI holds one Neon account by default. A profile adds another, and is a pointer: a
826
+ credentials file path, or the sentinel `"keyring"` when the secret is in the OS keyring.
826
827
 
827
828
  ```
828
829
  ~/.config/neon/
829
830
  ├── credentials.json # this IS the DEFAULT profile
830
831
  ├── credentials.work.json # created by `neon profile create work`
831
- └── profiles.json # created only once a second profile exists
832
+ └── profiles.json # created once a second profile exists, or DEFAULT is keyring
832
833
  ```
833
834
 
834
835
  ```bash
835
- neon profile create work # a browser sign-in, or an API key — see below
836
+ neon profile create work # a browser sign-in, or an API key — see below
837
+ neon profile create work --keyring # same, stored in the OS keyring
836
838
  neon profile list
837
839
  neon profile remove work
838
840
  ```
@@ -840,64 +842,102 @@ neon profile remove work
840
842
  ```console
841
843
  $ neon profile list
842
844
  Profiles
843
- ┌────────┬─────────┬───────────────────────┬─────────┬────────────────┬──────┬────────────────────────┐
844
- │ Active │ Name │ Account │ Auth │ Scope │ File │ Credentials │
845
- ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼────────────────────────┤
846
- │ * │ DEFAULT │ me@example.com │ oauth │ - │ ok │ credentials.json │
847
- ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼────────────────────────┤
848
- │ │ work │ me@example.com │ api key │ account │ ok │ credentials.work.json │
849
- ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼────────────────────────┤
850
- │ │ ci │ org-old-flower-827148 │ api key │ project proj-1 │ ok │ credentials.ci.json │
851
- └────────┴─────────┴───────────────────────┴─────────┴────────────────┴──────┴────────────────────────┘
845
+ ┌────────┬─────────┬───────────────────────┬─────────┬────────────────┬──────┬─────────┬────────────────────────┐
846
+ │ Active │ Name │ Account │ Auth │ Scope │ File │ Storage │ Credentials │
847
+ ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼─────────┼────────────────────────┤
848
+ │ * │ DEFAULT │ me@example.com │ oauth │ - │ ok │ file │ credentials.json │
849
+ ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼─────────┼────────────────────────┤
850
+ │ │ work │ me@example.com │ api key │ account │ ok │ keyring │ keyring │
851
+ ├────────┼─────────┼───────────────────────┼─────────┼────────────────┼──────┼─────────┼────────────────────────┤
852
+ │ │ ci │ org-abc-123 │ api key │ project proj-1 │ ok │ file │ credentials.ci.json │
853
+ └────────┴─────────┴───────────────────────┴─────────┴────────────────┴──────┴─────────┴────────────────────────┘
852
854
  ```
853
855
 
854
856
  `Scope` is what a key can reach; an OAuth session has none of its own, so it shows `-`. `File`
855
- says whether the credentials file can be read and understood — `ok`, `invalid` or `missing` —
856
- which is not the same as the credential still working; only using it shows that. The table shows
857
- the file name, `--output json` the full path.
857
+ says whether the stored credential can be read — `ok`, `invalid` or `missing` for a file;
858
+ `ok`, `invalid` or `unreadable` for a keyring item. A keyring get of null is `unreadable`, not missing:
859
+ the addon cannot tell those apart. `Storage` is `file` or `keyring`, from the profile's
860
+ pointer. The table shows the file name or `keyring`; `--output json` keeps the full path.
858
861
 
859
862
  Select one per invocation with `--profile`, or per shell with `NEON_PROFILE`. There is no `profile use` command and nothing is stored about which profile is "current", so what you type is always what runs.
860
863
 
861
- Entries in `profiles.json` are paths, and a path may point anywhere — which is how you adopt a directory you already have, without moving or re-authenticating anything:
864
+ Entries in `profiles.json` are pointers — a path, or the exact string `"keyring"` — and a path may point anywhere, which is how you adopt a directory you already have without moving or re-authenticating anything:
862
865
 
863
866
  ```json
864
867
  {
865
868
  "version": 1,
866
869
  "profiles": {
867
- "DEFAULT": { "credentials": "credentials.json" },
870
+ "DEFAULT": { "credentials": "keyring" },
868
871
  "work": { "credentials": "../neonctl-work/credentials.json" }
869
872
  }
870
873
  }
871
874
  ```
872
875
 
876
+ An unreadable `profiles.json` fails every command that needs a profile, including a
877
+ file-only DEFAULT. That file is the only record of where each account's credentials live,
878
+ so the CLI will not guess past it. Fix or delete it.
879
+
873
880
  `neon profile remove` revokes what the profile holds — an OAuth refresh token at the
874
881
  authorization server, or an API key this CLI minted — rather than only forgetting it locally. A
875
882
  key you supplied is the exception and stays live, because nothing records its id; the command
876
883
  says so. It asks for confirmation first, which `--yes` skips; without a terminal on stdin, in
877
884
  CI or behind a pipe, it refuses rather than prompting into the void. It deletes the credentials
878
885
  file only when the CLI created it: an adopted path like the one above is unlinked and left on
879
- disk, and the command says so. Removing the last named profile deletes `profiles.json`,
880
- returning you to the single-account layout. `neon profile remove DEFAULT` signs you out.
886
+ disk, and the command says so. A keyring profile's OS item is deleted when the store confirms
887
+ it is gone. If it cannot, remove still resets the profile and warns that a leftover may remain
888
+ in the OS store; it is unused once the profile is gone. Removing the last named profile
889
+ deletes `profiles.json` unless DEFAULT itself is keyring — that entry is the only record that
890
+ the secret is not in `credentials.json`. `neon profile remove DEFAULT` signs you out.
891
+
892
+ ### Where the secret is stored
893
+
894
+ The default is a credentials file. A profile uses the OS keyring (macOS Keychain, Windows
895
+ Credential Manager, Linux Secret Service) only when its `profiles.json` pointer is `"keyring"`.
896
+ That is per profile. Reads never migrate.
897
+
898
+ ```bash
899
+ neon auth --keyring # sign DEFAULT into the OS keyring
900
+ neon auth --keyring --profile work # sign work into the OS keyring
901
+ neon profile create work --keyring # create a named profile in the keyring
902
+ neon profile remove work --yes # drop a keyring profile, then create it again as a file
903
+ ```
904
+
905
+ File to keyring is `neon auth --keyring` or `neon profile create … --keyring`: a new
906
+ sign-in, then the previous credential is revoked and the owned file is deleted.
907
+ `create` on an existing name always revokes after a successful write. `auth` revokes
908
+ when it writes to the keyring, including a re-login that follows an existing pointer.
909
+ `auth` that overwrites a file does not. Keyring to file is `remove`, then create or
910
+ auth again. `create` and `auth` without `--keyring` follow an existing `"keyring"`
911
+ pointer, so a keyring profile cannot leave the OS store until `remove` succeeds.
912
+
913
+ `--api-key` and `NEON_API_KEY` skip both stores. A GitHub-release `neon-<platform>`
914
+ binary recognizes a `"keyring"` pointer and refuses: it cannot load the OS keyring addon.
915
+ Use the npm-installed `neon`. Older releases treat the sentinel as a relative path.
916
+
917
+ A `"keyring"` pointer whose OS item cannot be read is not treated as signed-out. Commands that
918
+ would otherwise open a browser fail: could not read the OS keyring item. Unlock it and
919
+ retry, or run `neon auth --profile DEFAULT`. To reset the profile: `neon profile remove DEFAULT --yes`.
920
+ A missing `credentials.json` with no `profiles.json` is still
921
+ signed-out, and those commands start OAuth.
881
922
 
882
923
  ### A profile holds either a sign-in or an API key
883
924
 
884
925
  `neon profile create` makes a profile, and how you call it decides which kind of credential it holds. A key-backed profile is what you want for an agent, a shared machine, or anything that must never be interrupted by a browser:
885
926
 
886
927
  ```bash
887
- neon profile create work # sign in with the browser, like `neon auth`
928
+ neon profile create work # sign in; replaces work if it already exists
929
+ neon profile create work --keyring # same, stored in the OS keyring
888
930
  neon profile create work --api-key "$KEY" # store a key you already have
889
931
  echo "$KEY" | neon profile create work --api-key - # or pipe it, keeping it out of argv
890
932
  neon profile create ci --mint # sign in once, keep only a minted key
891
933
  neon profile create ci --mint --org-id org-abc-123 # minted for an organization
892
934
  neon profile create ci --mint --project-id proj-1 # minted for one project only
893
- neon profile create work --force # replace it, revoking what it holds now
894
935
  neon profile rotate-key work # mint a replacement, revoke the old one
895
936
  ```
896
937
 
897
- `--force` is not only a local edit: replacing a profile revokes the credential it held, so a key
898
- this CLI minted stops working everywhere it was pasted, and an OAuth session is signed out.
899
- Without `--force`, `create` refuses and names what would be revoked. To keep a working profile
900
- and swap only its key, use `rotate-key`.
938
+ Replacing a profile revokes the credential it held, so a key this CLI minted stops working
939
+ everywhere it was pasted, and an OAuth session is signed out. To keep a working profile and
940
+ swap only its key, use `rotate-key`.
901
941
 
902
942
  `create` and `rotate-key` print the profile they wrote, so an agent needn't follow up with
903
943
  `list`. Under `--output json` that is a record, and it never carries the secret:
@@ -926,7 +966,7 @@ the key never leaves the CLI.
926
966
  { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
927
967
  ```
928
968
 
929
- Nothing is carried over when a profile is replaced, so one profile can never hold two credentials — or two different accounts. The secret stays in that file and never goes into `profiles.json`, so listing profiles cannot leak one. Both files are written owner-only through a temporary file and a rename, which also repairs the permissions of a file created too permissively.
969
+ Nothing is carried over when a profile is replaced, so one profile can never hold two credentials — or two different accounts. The secret stays in the credentials file or the OS keyring and never goes into `profiles.json`, so listing profiles cannot leak one. Files are written owner-only through a temporary file and a rename, which also repairs the permissions of a file created too permissively.
930
970
 
931
971
  `--mint` is the one to reach for. It signs you in through the browser once, mints a key with that session, stores only the key, and signs the session back out — so afterwards nothing about the profile can open a browser, and no half-forgotten login is left behind. `--org-id` and `--project-id` narrow what the minted key can reach, exactly as they do on [`neon api-keys create`](#api-keys); a project-scoped key cannot create projects, mint keys, or read any other project.
932
972
 
@@ -934,11 +974,11 @@ Every key is verified against the API before it is stored, and the account it be
934
974
 
935
975
  `rotate-key` mints at the scope the profile already has — replacing an org key with an account key would quietly widen everything it reaches — and stores the new key before revoking the old one, so a failed write leaves the old key working.
936
976
 
937
- One thing it cannot do: **an organization key cannot mint its own replacement.** Neon only accepts a personal credential when creating organization keys, so rotating an org- or project-scoped profile means signing in again — `neon profile create ci --mint --org-id org-abc-123 --force`. `rotate-key` checks this before minting and says so, rather than letting the API answer with a rule you had no reason to expect.
977
+ One thing it cannot do: **an organization key cannot mint its own replacement.** Neon only accepts a personal credential when creating organization keys, so rotating an org- or project-scoped profile means signing in again — `neon profile create ci --mint --org-id org-abc-123`. `rotate-key` checks this before minting and says so, rather than letting the API answer with a rule you had no reason to expect.
938
978
 
939
979
  Two things the CLI cannot do for a key you supplied rather than minted. It cannot revoke it, because `GET /api_keys` exposes no prefix and a stored secret cannot be matched to a listing entry, so both `rotate-key` and `profile remove` say the old key is still live and point you at `neon api-keys list`. For a key you supplied it records the organization the API reports, but cannot know whether that key was narrowed to a single project — so `rotate-key` will not suggest an organization-wide replacement without telling you to check `neon api-keys list` first.
940
980
 
941
- If a stored key stops working there is nothing to refresh, so recovery is one browser sign-in: `neon profile create work --mint --force`.
981
+ If a stored key stops working there is nothing to refresh, so recovery is one browser sign-in: `neon profile create work --mint`.
942
982
 
943
983
  ### Which credential an invocation uses
944
984
 
@@ -1089,7 +1129,7 @@ Global options are supported with any Neon CLI command.
1089
1129
 
1090
1130
  - <a id="config-dir"></a>`--config-dir`
1091
1131
 
1092
- Specifies the path to the `neon` configuration directory, which holds the `credentials.json` written by `neon auth`. The default is `$XDG_CONFIG_HOME/neon`, or `~/.config/neon`; run `neon --help` to see the resolved path. This option is only necessary if you keep your configuration somewhere else.
1132
+ Specifies the path to the `neon` configuration directory, which holds the `credentials.json` written by `neon auth` and, when a second profile exists or DEFAULT is keyring, `profiles.json`. The default is `$XDG_CONFIG_HOME/neon`, or `~/.config/neon`; run `neon --help` to see the resolved path. This option is only necessary if you keep your configuration somewhere else.
1093
1133
 
1094
1134
  The directory was called `neonctl` before the CLI was renamed. An existing one is still read, and is used **in place** — nothing is moved or copied, so there is never a second credentials file to go stale. A directory you pass explicitly is used exactly as given and never falls back to the legacy name, so pointing a CI run at a scratch directory cannot pick up local credentials.
1095
1135
 
@@ -1,4 +1,4 @@
1
- import "./profiles-Ir29rqns.js";
1
+ import "./profiles-CVXNvU2r.js";
2
2
  //#region ../../internals/cli-core/dist/auth_selection.js
3
3
  /**
4
4
  * # Which credential an invocation authenticates with
@@ -2573,12 +2573,14 @@ const cmdEndPipeline = {
2573
2573
  }
2574
2574
  const isSyncOrPlaceholder = rs.fields.length === 0 && rs.command === "" && rs.rows.length === 0;
2575
2575
  const isCommandOnly = rs.fields.length === 0 && rs.rows.length === 0 && rs.command !== "";
2576
- if (!isSyncOrPlaceholder && !isCommandOnly) if (rs.fields.length === 0 && rs.rows.length > 0) {
2577
- if (!ctx.settings.popt.topt.tuplesOnly) {
2578
- process.stdout.write("--\n");
2579
- process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
2580
- }
2581
- } else await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
2576
+ if (!isSyncOrPlaceholder && !isCommandOnly) {
2577
+ if (rs.fields.length === 0 && rs.rows.length > 0) {
2578
+ if (!ctx.settings.popt.topt.tuplesOnly) {
2579
+ process.stdout.write("--\n");
2580
+ process.stdout.write(`(${rs.rows.length} ${rs.rows.length === 1 ? "row" : "rows"})\n\n`);
2581
+ }
2582
+ } else await alignedPrinter.printQuery(rs, ctx.settings.popt, process.stdout);
2583
+ }
2582
2584
  } else if (!errorRendered) {
2583
2585
  const reason = r.reason;
2584
2586
  const isAborted = typeof reason === "object" && reason !== null && reason.pipelineAborted === true;
@@ -0,0 +1,409 @@
1
+ import { b as writeSecretFile, f as profilesFilePath, v as CRED_STORAGE_FILE, y as CRED_STORAGE_KEYRING } from "./profiles-CVXNvU2r.js";
2
+ import { o as isOwnedCredentialPath } from "./paths-g3DRqJiD.js";
3
+ import { tryLoadKeyring } from "../keyring.js";
4
+ import { existsSync, readFileSync, rmSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ //#region ../../internals/cli-core/dist/credentials.js
8
+ /**
9
+ * # Stored credentials — one file per account, two kinds
10
+ *
11
+ * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says
12
+ * what kind of credential it holds. Adding API-key support this way rather than adding a
13
+ * second pointer to `profiles.json` keeps a profile what it already was — one name, one path
14
+ * — and means `profiles.json` needs no schema change at all.
15
+ *
16
+ * ```json
17
+ * // oauth: every file written before this existed. An absent `type` means this.
18
+ * { "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
19
+ *
20
+ * // api_key, stored by `neon profile create --api-key`
21
+ * { "type": "api_key", "api_key": "napi_…", "user_id": "…" }
22
+ *
23
+ * // api_key minted by `--mint --org-id`, which records the scope it was issued at
24
+ * { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
25
+ * ```
26
+ *
27
+ * ## One profile, one kind
28
+ *
29
+ * A credentials file holds an API key or an OAuth session, never both, and `type` states
30
+ * which. An earlier draft let the two coexist — the idea being that a key could keep the
31
+ * session it was minted from and so rotate without a browser. It did not survive review, for
32
+ * two reasons that are worth recording so nobody rebuilds it:
33
+ *
34
+ * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key
35
+ * failed to mint and never fell back to the session sitting beside it.
36
+ * 2. **It could mix accounts.** Nothing compared the identity of the credential being written
37
+ * with the one already there, so a profile could hold one account's session and another's
38
+ * key, told apart only by a single string. Flip or lose `type` and the profile silently
39
+ * becomes a different person.
40
+ *
41
+ * Recovery from a dead key is therefore one browser login — `neon profile create <name>
42
+ * --mint` — which is what the retained session was supposed to save and never did.
43
+ *
44
+ * ## Older releases
45
+ *
46
+ * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and
47
+ * looks for `access_token`. An `api_key` profile has none, so an older release falls through
48
+ * to its browser login rather than crashing. That it does not crash is why `credentials`
49
+ * stays a required pointer: an entry without one makes 2.41 and 2.42 throw
50
+ * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.
51
+ */
52
+ const OAUTH = "oauth";
53
+ const API_KEY = "api_key";
54
+ const credentialLabel = (at) => at.storage === "keyring" ? `the OS keyring item for profile "${at.profile}"` : at.path;
55
+ /**
56
+ * Which credential in this file authenticates, by declaration alone.
57
+ *
58
+ * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot
59
+ * interpret is a misconfiguration the user has to see: treating it as OAuth would send them
60
+ * to a browser login that silently replaces a credential they meant to keep, and treating it
61
+ * as an API key would authenticate with whatever `api_key` happened to be there.
62
+ *
63
+ * This deliberately does not check that an `api_key` file has a key — `neon profile list`
64
+ * needs the kind of a file it is not about to authenticate with, and must be able to report a
65
+ * broken one rather than throwing halfway through a table.
66
+ */
67
+ const credentialKind = (credentials, at, store = "file") => {
68
+ const declared = credentials.type;
69
+ if (declared === void 0 || declared === "oauth") return OAUTH;
70
+ if (declared === "api_key") return API_KEY;
71
+ throw new Error(`${credentialLabel(at)} declares a "type" this version does not understand. Expected "${OAUTH}" or "${API_KEY}". ${credentialsRepairHint(at, store)}`);
72
+ };
73
+ const credentialsRepairHint = (at, store = "file") => store === "keyring" ? `Replace it deliberately with \`neon profile create ${at.profile}\`, or remove the profile with \`neon profile remove ${at.profile}\`.` : `Replace it deliberately with \`neon profile create ${at.profile}\`, or delete the file.`;
74
+ /**
75
+ * Resolve what to authenticate with, validating that the declared kind is actually usable.
76
+ *
77
+ * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user
78
+ * asked for a key, and quietly opening a browser instead would replace the credential they
79
+ * were trying to fix.
80
+ */
81
+ const interpretCredentials = (credentials, at, store = "file") => {
82
+ if (credentialKind(credentials, at, store) === "oauth") return { kind: OAUTH };
83
+ const apiKey = nonEmpty(credentials.api_key);
84
+ if (apiKey === void 0) throw new Error(`${credentialLabel(at)} declares "type": "${API_KEY}" but has no "api_key" value. ${credentialsRepairHint(at, store)}`);
85
+ return {
86
+ kind: API_KEY,
87
+ apiKey
88
+ };
89
+ };
90
+ /**
91
+ * Read and classify a credentials file, without deciding what to do about it.
92
+ *
93
+ * A permission or I/O error still throws: there may be a perfectly good credential here that
94
+ * we cannot see, and treating that as absent would send the user to a browser login that
95
+ * overwrites it.
96
+ */
97
+ /** Discard parser details because V8 may quote secret material near a syntax error. */
98
+ const parseCredentialsJson = (contents, label) => {
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(contents);
102
+ } catch {
103
+ return {
104
+ kind: "unusable",
105
+ reason: `${label} is not valid JSON, so the credential in it cannot be read`
106
+ };
107
+ }
108
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {
109
+ kind: "unusable",
110
+ reason: `${label} does not contain a credentials object`
111
+ };
112
+ return {
113
+ kind: "ok",
114
+ credentials: parsed
115
+ };
116
+ };
117
+ const inspectCredentials = (path) => {
118
+ let contents;
119
+ try {
120
+ contents = readFileSync(path, "utf8");
121
+ } catch (err) {
122
+ if (err.code === "ENOENT") return { kind: "absent" };
123
+ throw err;
124
+ }
125
+ return parseCredentialsJson(contents, path);
126
+ };
127
+ /**
128
+ * The credential at `path`, or `null` when the file is not there.
129
+ *
130
+ * A damaged file is an error, not an absence. Treating it as absent — which is what this used to
131
+ * do — meant any read-only command could repair it by starting a browser sign-in and overwriting
132
+ * it, **possibly as a different account**, with the user never having asked for a repair and no
133
+ * way back to whatever was in the file. Failing here costs one deliberate command; the message
134
+ * names it.
135
+ *
136
+ * `profile list` and telemetry use {@link inspectCredentials} instead, because describing a
137
+ * broken credential is not the same as using one.
138
+ */
139
+ const readCredentials = (at) => {
140
+ const read = inspectCredentials(at.path);
141
+ if (read.kind === "unusable") throw new Error(`${read.reason}. ${credentialsRepairHint(at)}`);
142
+ return read.kind === "ok" ? read.credentials : null;
143
+ };
144
+ const writeCredentials = (path, credentials) => {
145
+ writeSecretFile(path, JSON.stringify(credentials));
146
+ };
147
+ /**
148
+ * Build an `api_key` credentials object. Nothing from a previous credential is carried over.
149
+ *
150
+ * The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint
151
+ * the replacement on the same endpoint, and an org or project key minted as an account key
152
+ * would silently widen what the profile reaches.
153
+ */
154
+ const apiKeyCredentials = ({ apiKey, keyId, userId, scope }) => ({
155
+ type: API_KEY,
156
+ api_key: apiKey,
157
+ ...keyId !== void 0 ? { key_id: keyId } : {},
158
+ ...userId !== void 0 ? { user_id: userId } : {},
159
+ ...scope?.orgId !== void 0 ? { org_id: scope.orgId } : {},
160
+ ...scope?.projectId !== void 0 ? { project_id: scope.projectId } : {}
161
+ });
162
+ /** The scope recorded on a stored credential. */
163
+ const scopeOf = (credentials) => ({
164
+ ...typeof credentials.org_id === "string" ? { orgId: credentials.org_id } : {},
165
+ ...typeof credentials.project_id === "string" ? { projectId: credentials.project_id } : {}
166
+ });
167
+ /** How to describe a scope in output. */
168
+ const describeScope = (scope) => {
169
+ if (scope.projectId !== void 0) return `project ${scope.projectId}`;
170
+ if (scope.orgId !== void 0) return `org ${scope.orgId}`;
171
+ return "account";
172
+ };
173
+ function nonEmpty(value) {
174
+ if (typeof value !== "string") return void 0;
175
+ const trimmed = value.trim();
176
+ return trimmed === "" ? void 0 : trimmed;
177
+ }
178
+ /**
179
+ * Whether a stored credential is the same secret as the one about to replace it.
180
+ *
181
+ * Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it
182
+ * would revoke the credential the command has just committed to. Trimmed on both sides, because
183
+ * a key read from a file or a pipe arrives with a trailing newline.
184
+ */
185
+ const isSameCredential = (existingKey, replacementKey) => {
186
+ if (existingKey === void 0 || replacementKey === void 0) return false;
187
+ const trimmed = existingKey.trim();
188
+ return trimmed !== "" && trimmed === replacementKey.trim();
189
+ };
190
+ //#endregion
191
+ //#region ../../internals/cli-core/dist/credential_store.js
192
+ const KEYRING_SERVICE = "com.neon.neon-cli";
193
+ /** Hashing the resolved profiles directory isolates config roots while keeping profile names visible. */
194
+ const keyringAccount = (configDir, profile) => `cli:${createHash("sha256").update(dirname(profilesFilePath(configDir))).digest("hex")}:${profile}`;
195
+ var KeyringUnavailableError = class extends Error {
196
+ constructor(profile, kind = "read") {
197
+ const loaded = "This CLI cannot use the OS keyring.";
198
+ super(profile === void 0 ? `${loaded} Drop \`--keyring\` to keep the credential in a file.` : kind === "write" ? `${loaded} Remove the profile with \`neon profile remove ${profile} --yes\`.` : `${loaded} Use --api-key or NEON_API_KEY. If this is a standalone neon binary, use the npm-installed neon instead. To reset the profile: \`neon profile remove ${profile} --yes\`.`);
199
+ this.name = "KeyringUnavailableError";
200
+ }
201
+ };
202
+ var KeyringUnreadableError = class extends Error {
203
+ constructor(profile) {
204
+ const replace = `\`neon auth --profile ${profile}\``;
205
+ super(`Could not read the OS keyring item for profile "${profile}". Unlock the keyring and retry, or run ${replace}. To reset the profile: \`neon profile remove ${profile} --yes\`.`);
206
+ this.name = "KeyringUnreadableError";
207
+ }
208
+ };
209
+ var KeyringClearError = class extends Error {
210
+ constructor(profile, kind = "visible") {
211
+ const recovery = `\`neon profile remove ${profile} --yes\``;
212
+ super(kind === "unconfirmed" ? `Could not confirm the OS keyring item for profile "${profile}" is gone. The OS store does not distinguish a missing item from denied access. Unlock the OS keyring and retry, or reset the profile with ${recovery} (a leftover may remain; it is unused once the profile is gone).` : `Could not clear the OS keyring item for profile "${profile}". Unlock the OS keyring and retry, or reset the profile with ${recovery} (a leftover may remain; it is unused once the profile is gone).`);
213
+ this.name = "KeyringClearError";
214
+ }
215
+ };
216
+ const deleteFileIfPresent = (path) => {
217
+ if (!existsSync(path)) return false;
218
+ rmSync(path);
219
+ return true;
220
+ };
221
+ const inspectKeyringItem = (keyring, account, label) => {
222
+ if (keyring === null) return { kind: "absent" };
223
+ let raw;
224
+ try {
225
+ raw = keyring.get(KEYRING_SERVICE, account);
226
+ } catch {
227
+ return { kind: "absent" };
228
+ }
229
+ if (raw === null) return { kind: "absent" };
230
+ return parseCredentialsJson(raw, label);
231
+ };
232
+ const createCredentialStore = (dir, options = {}) => {
233
+ const keyring = options.keyring ?? null;
234
+ const accountFor = (profile) => keyringAccount(dir, profile);
235
+ const assertKeyringWritable = (profile) => {
236
+ if (keyring === null) throw new KeyringUnavailableError(profile, "write");
237
+ };
238
+ const setKeyringOrRollback = (profile, credentials) => {
239
+ assertKeyringWritable(profile);
240
+ const kr = keyring;
241
+ if (kr === null) throw new KeyringUnavailableError(profile, "write");
242
+ const account = accountFor(profile);
243
+ const label = `profile "${profile}"`;
244
+ let previous = null;
245
+ try {
246
+ previous = kr.get(KEYRING_SERVICE, account);
247
+ } catch {
248
+ previous = null;
249
+ }
250
+ try {
251
+ kr.set(KEYRING_SERVICE, account, JSON.stringify(credentials));
252
+ } catch {
253
+ throw new KeyringUnavailableError();
254
+ }
255
+ try {
256
+ if (kr.get("com.neon.neon-cli", account) === null) throw new Error(`Wrote credentials to the OS keyring for ${label} but could not read them back.`);
257
+ } catch (err) {
258
+ if (previous !== null) {
259
+ try {
260
+ kr.set(KEYRING_SERVICE, account, previous);
261
+ } catch {
262
+ throw new KeyringClearError(profile, "visible");
263
+ }
264
+ let restored = null;
265
+ try {
266
+ restored = kr.get(KEYRING_SERVICE, account);
267
+ } catch {
268
+ restored = null;
269
+ }
270
+ if (restored === null) throw new KeyringClearError(profile, "visible");
271
+ }
272
+ throw err instanceof Error ? err : new Error(String(err));
273
+ }
274
+ };
275
+ const removeKeyringItem = (profile, required, account = accountFor(profile)) => {
276
+ if (keyring === null) {
277
+ if (required) throw new KeyringUnavailableError(profile, "write");
278
+ return "unconfirmed";
279
+ }
280
+ let raw;
281
+ try {
282
+ raw = keyring.get(KEYRING_SERVICE, account);
283
+ } catch (err) {
284
+ if (!required) return "unconfirmed";
285
+ throw err instanceof Error ? err : new Error(String(err));
286
+ }
287
+ if (raw === null) {
288
+ if (required) throw new KeyringClearError(profile, "unconfirmed");
289
+ return "unconfirmed";
290
+ }
291
+ let deleted;
292
+ try {
293
+ deleted = keyring.delete(KEYRING_SERVICE, account);
294
+ } catch (err) {
295
+ if (!required) return "unconfirmed";
296
+ throw err instanceof Error ? err : new Error(String(err));
297
+ }
298
+ let still;
299
+ try {
300
+ still = keyring.get(KEYRING_SERVICE, account);
301
+ } catch (err) {
302
+ if (!required) return "unconfirmed";
303
+ throw err instanceof Error ? err : new Error(String(err));
304
+ }
305
+ if (!deleted || still !== null) {
306
+ if (required) throw new KeyringClearError(profile, "visible");
307
+ return "left";
308
+ }
309
+ return "cleared";
310
+ };
311
+ const inspect = (at) => {
312
+ if (at.storage === "keyring") {
313
+ if (keyring === null) return {
314
+ file: "unreadable",
315
+ storage: CRED_STORAGE_KEYRING,
316
+ credentials: null,
317
+ reason: new KeyringUnavailableError(at.profile).message
318
+ };
319
+ const keyringRead = inspectKeyringItem(keyring, accountFor(at.profile), credentialLabel(at));
320
+ if (keyringRead.kind === "ok") return {
321
+ file: "ok",
322
+ storage: CRED_STORAGE_KEYRING,
323
+ credentials: keyringRead.credentials
324
+ };
325
+ if (keyringRead.kind === "unusable") return {
326
+ file: "unreadable",
327
+ storage: CRED_STORAGE_KEYRING,
328
+ credentials: null,
329
+ reason: keyringRead.reason
330
+ };
331
+ return {
332
+ file: "unreadable",
333
+ storage: CRED_STORAGE_KEYRING,
334
+ credentials: null,
335
+ reason: new KeyringUnreadableError(at.profile).message
336
+ };
337
+ }
338
+ const fileRead = inspectCredentials(at.path);
339
+ return {
340
+ file: fileRead.kind === "ok" ? "ok" : fileRead.kind === "absent" ? "missing" : "invalid",
341
+ storage: CRED_STORAGE_FILE,
342
+ credentials: fileRead.kind === "ok" ? fileRead.credentials : null,
343
+ ...fileRead.kind === "unusable" ? { reason: fileRead.reason } : {}
344
+ };
345
+ };
346
+ const read = (at) => {
347
+ if (at.storage === "keyring") {
348
+ if (keyring === null) throw new KeyringUnavailableError(at.profile);
349
+ let raw;
350
+ try {
351
+ raw = keyring.get(KEYRING_SERVICE, accountFor(at.profile));
352
+ } catch {
353
+ throw new KeyringUnreadableError(at.profile);
354
+ }
355
+ if (raw === null) throw new KeyringUnreadableError(at.profile);
356
+ const parsed = parseCredentialsJson(raw, credentialLabel(at));
357
+ if (parsed.kind === "unusable") throw new Error(parsed.reason);
358
+ if (parsed.kind !== "ok") throw new KeyringUnreadableError(at.profile);
359
+ return {
360
+ credentials: parsed.credentials,
361
+ backend: CRED_STORAGE_KEYRING,
362
+ profile: at.profile
363
+ };
364
+ }
365
+ const credentials = readCredentials(at);
366
+ if (credentials === null) return null;
367
+ return {
368
+ credentials,
369
+ backend: CRED_STORAGE_FILE,
370
+ path: at.path,
371
+ profile: at.profile
372
+ };
373
+ };
374
+ const write = (at, credentials) => {
375
+ if (at.storage === "keyring") {
376
+ setKeyringOrRollback(at.profile, credentials);
377
+ return {
378
+ credentials,
379
+ backend: CRED_STORAGE_KEYRING,
380
+ profile: at.profile
381
+ };
382
+ }
383
+ writeCredentials(at.path, credentials);
384
+ return {
385
+ credentials,
386
+ backend: CRED_STORAGE_FILE,
387
+ path: at.path,
388
+ profile: at.profile
389
+ };
390
+ };
391
+ const del = (at, deleteOptions) => {
392
+ const required = deleteOptions?.required !== false;
393
+ if (at.storage === "keyring") return removeKeyringItem(at.profile, required, deleteOptions?.account);
394
+ if (!isOwnedCredentialPath(dir, at.path)) return "skipped";
395
+ return deleteFileIfPresent(at.path) ? "cleared" : "absent";
396
+ };
397
+ return {
398
+ inspect,
399
+ read,
400
+ write,
401
+ delete: del,
402
+ assertKeyringWritable
403
+ };
404
+ };
405
+ //#endregion
406
+ //#region src/credential_io.ts
407
+ const storeFor = (dir) => createCredentialStore(dir, { keyring: tryLoadKeyring() });
408
+ //#endregion
409
+ export { OAUTH as a, describeScope as c, scopeOf as d, API_KEY as i, interpretCredentials as l, KEYRING_SERVICE as n, apiKeyCredentials as o, keyringAccount as r, credentialLabel as s, storeFor as t, isSameCredential as u };
@@ -148,4 +148,4 @@ const isOwnedCredentialPath = (configDirectory, file) => {
148
148
  return legacy !== void 0 && isInsideConfigDir(legacy, file);
149
149
  };
150
150
  //#endregion
151
- export { isOwnedCredentialPath as a, isInsideConfigDir as i, credentialsPath as n, resolveConfigFile as o, defaultDir as r, CREDENTIALS_FILE as t };
151
+ export { isInsideConfigDir as a, defaultDir as i, configDir as n, isOwnedCredentialPath as o, credentialsPath as r, resolveConfigFile as s, CREDENTIALS_FILE as t };