neon 3.2.1 → 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.
- package/README.md +71 -31
- package/dist/_chunks/{auth_selection-DGgq6ifc.js → auth_selection-CV_3n1du.js} +1 -1
- package/dist/_chunks/{cmd_pipeline-CUbBO9U_.js → cmd_pipeline-CEk93dGN.js} +8 -6
- package/dist/_chunks/credential_io-D2vw1UOw.js +409 -0
- package/dist/_chunks/{paths-DMq0Lt7a.js → paths-g3DRqJiD.js} +1 -1
- package/dist/_chunks/{profiles-Ir29rqns.js → profiles-CVXNvU2r.js} +106 -66
- package/dist/_chunks/{psql-DWH-kc69.js → psql-BNljLda2.js} +8 -7
- package/dist/_chunks/{rolldown-runtime-D7D4PA-g.js → rolldown-runtime-8H4AJuhK.js} +1 -0
- package/dist/analytics.js +19 -5
- package/dist/auth_context.js +25 -24
- package/dist/commands/api.js +1 -1
- package/dist/commands/api_keys.js +1 -1
- package/dist/commands/auth.js +121 -80
- package/dist/commands/bootstrap.js +2 -2
- package/dist/commands/branches.js +1 -1
- package/dist/commands/bucket.js +1 -1
- package/dist/commands/checkout.js +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/connection_string.js +1 -1
- package/dist/commands/data_api.js +1 -1
- package/dist/commands/databases.js +1 -1
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/diff.js +1 -1
- package/dist/commands/env.js +1 -1
- package/dist/commands/functions.js +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/ip_allow.js +1 -1
- package/dist/commands/link.js +1 -1
- package/dist/commands/logs.js +1 -1
- package/dist/commands/neon_auth.js +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/commands/operations.js +1 -1
- package/dist/commands/orgs.js +1 -1
- package/dist/commands/profile.js +183 -202
- package/dist/commands/projects.js +1 -1
- package/dist/commands/psql.js +1 -1
- package/dist/commands/roles.js +1 -1
- package/dist/commands/schema_diff.js +15 -13
- package/dist/commands/set_context.js +1 -1
- package/dist/commands/snapshots.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/user.js +1 -1
- package/dist/commands/vpc_endpoints.js +1 -1
- package/dist/config.js +1 -1
- package/dist/credential_io.js +2 -0
- package/dist/index.js +27 -25
- package/dist/init/auth.js +14 -25
- package/dist/init/phases/setup.js +5 -5
- package/dist/keyring.js +42 -0
- package/dist/psql/cli.js +1 -1
- package/dist/psql/command/cmd_connect.js +4 -3
- package/dist/psql/command/cmd_copy.js +65 -62
- package/dist/psql/command/cmd_io.js +1 -1
- package/dist/psql/command/cmd_pipeline.js +1 -1
- package/dist/psql/command/dispatch.js +1 -1
- package/dist/psql/core/common.js +1 -1
- package/dist/psql/core/mainloop.js +1 -1
- package/dist/psql/core/startup.js +1 -1
- package/dist/psql/index.js +1 -1
- package/dist/psql/io/psqlrc.js +13 -11
- package/dist/psql/print/aligned.js +9 -5
- package/dist/psql/wire/tls.js +11 -9
- package/dist/retire_credential.js +86 -0
- package/dist/utils/inspect_db.js +1 -1
- package/dist/utils/middlewares.js +1 -1
- package/package.json +4 -3
- package/dist/_chunks/credentials-MYdHdKah.js +0 -188
- package/dist/_chunks/secure_file-BucZj4yQ.js +0 -39
package/README.md
CHANGED
|
@@ -673,7 +673,7 @@ $ neon init
|
|
|
673
673
|
|
|
674
674
|
Run in a terminal it prompts you through those steps. This is what the retired `neon-init` package used to do; `npx neon init` replaces it.
|
|
675
675
|
|
|
676
|
-
Two side effects worth knowing before you run it. It **installs or upgrades `
|
|
676
|
+
Two side effects worth knowing before you run it. It **installs or upgrades `neon` globally**, with whichever package manager invoked it — the flow drives Neon by shelling out to the CLI rather than calling the API in-process. And it **writes `.neon`** in the project directory, the same context file `neon link` and `neon checkout` use.
|
|
677
677
|
|
|
678
678
|
### Agent mode
|
|
679
679
|
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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 │
|
|
849
|
-
|
|
850
|
-
│ │ ci │ org-
|
|
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
|
|
856
|
-
|
|
857
|
-
the
|
|
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
|
|
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": "
|
|
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.
|
|
880
|
-
|
|
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
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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)
|
|
2577
|
-
if (
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
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 {
|
|
151
|
+
export { isInsideConfigDir as a, defaultDir as i, configDir as n, isOwnedCredentialPath as o, credentialsPath as r, resolveConfigFile as s, CREDENTIALS_FILE as t };
|