neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
package/dist/commands/profile.js
CHANGED
|
@@ -1,913 +1,682 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { credentialInputs } from "../_shared/auth_selection.js";
|
|
5
|
-
import { API_KEY, apiKeyCredentials, credentialKind, describeScope, inspectCredentials, interpretCredentials, isSameCredential, OAUTH, readCredentials, scopeOf, writeCredentials, } from "../_shared/credentials.js";
|
|
6
|
-
import { assertProfilesUsable, assertValidProfileName, DEFAULT_PROFILE, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, resolveProfile, selectProfileName, upsertProfile, } from "../_shared/profiles.js";
|
|
7
|
-
import { writeSecretFile } from "../_shared/secure_file.js";
|
|
8
|
-
import { getApiClient, isNeonApiError } from "../api.js";
|
|
9
|
-
import { auth, revokeToken } from "../auth.js";
|
|
10
|
-
import { setAuthContext } from "../auth_context.js";
|
|
11
|
-
import { isInsideConfigDir } from "../config.js";
|
|
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";
|
|
12
4
|
import { isCi } from "../env.js";
|
|
13
5
|
import { log } from "../log.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
6
|
+
import { getApiClient, isNeonApiError } from "../api.js";
|
|
7
|
+
import { i as isInsideConfigDir } from "../_chunks/paths-DMq0Lt7a.js";
|
|
8
|
+
import "../config.js";
|
|
9
|
+
import { setAuthContext } from "../auth_context.js";
|
|
10
|
+
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";
|
|
16
14
|
import { writer } from "../writer.js";
|
|
15
|
+
import { noPassthrough, single } from "../utils/flags.js";
|
|
17
16
|
import { orgIdForProject } from "./api_keys.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
})
|
|
80
|
-
.strict()
|
|
81
|
-
.check(noPassthrough("profile rotate-key")), async (args) => await rotateKey(args))
|
|
82
|
-
.command("remove <name>", "Revoke a profile's credential where possible, and remove it", (y) => y
|
|
83
|
-
.positional("name", {
|
|
84
|
-
describe: "Profile to remove",
|
|
85
|
-
type: "string",
|
|
86
|
-
demandOption: true,
|
|
87
|
-
})
|
|
88
|
-
.option("yes", {
|
|
89
|
-
alias: "y",
|
|
90
|
-
describe: "Skip the confirmation prompt",
|
|
91
|
-
type: "boolean",
|
|
92
|
-
default: false,
|
|
93
|
-
})
|
|
94
|
-
.strict()
|
|
95
|
-
.check(noPassthrough("profile remove")), async (args) => await remove(args))
|
|
96
|
-
.demandCommand(1, "Run `neon profile --help` to see the subcommands.");
|
|
97
|
-
export const handler = (_args) => {
|
|
98
|
-
/* subcommands only */
|
|
99
|
-
};
|
|
17
|
+
import { identityFromAuthDetails, isApiKeyMethod, mintedKeyName, notAnApiKeyMessage } from "../profile_keys.js";
|
|
18
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
19
|
+
import { basename } from "node:path";
|
|
20
|
+
import prompts from "prompts";
|
|
21
|
+
//#region src/commands/profile.ts
|
|
22
|
+
var profile_exports = /* @__PURE__ */ __exportAll({
|
|
23
|
+
aliases: () => aliases,
|
|
24
|
+
builder: () => builder,
|
|
25
|
+
command: () => command,
|
|
26
|
+
describe: () => describe,
|
|
27
|
+
handler: () => handler
|
|
28
|
+
});
|
|
29
|
+
const command = "profile";
|
|
30
|
+
const aliases = ["profiles"];
|
|
31
|
+
const describe = "Manage named sets of Neon credentials";
|
|
32
|
+
const builder = (argv) => argv.usage("$0 profile <sub-command> [options]").command("list", "List profiles, the account each holds, and where its credentials live", (y) => y.strict().check(noPassthrough("profile list")), async (args) => await list(args)).command("create <name>", "Create a profile. It holds either a browser sign-in or an API key, never both", (y) => y.positional("name", {
|
|
33
|
+
describe: "Name for the new profile",
|
|
34
|
+
type: "string",
|
|
35
|
+
demandOption: true
|
|
36
|
+
}).options({
|
|
37
|
+
"api-key": {
|
|
38
|
+
describe: "API key to store, or \"-\" to read it from stdin",
|
|
39
|
+
defaultDescription: "none; this command ignores NEON_API_KEY",
|
|
40
|
+
type: "string",
|
|
41
|
+
nargs: 1
|
|
42
|
+
},
|
|
43
|
+
mint: {
|
|
44
|
+
describe: "Sign in once in the browser, then store a freshly minted API key and nothing else",
|
|
45
|
+
type: "boolean",
|
|
46
|
+
default: false
|
|
47
|
+
},
|
|
48
|
+
"org-id": {
|
|
49
|
+
describe: "With --mint, mint a key for this organization instead of your account",
|
|
50
|
+
type: "string",
|
|
51
|
+
coerce: single("org-id")
|
|
52
|
+
},
|
|
53
|
+
"project-id": {
|
|
54
|
+
describe: "With --mint, mint a key that can access only this project",
|
|
55
|
+
type: "string",
|
|
56
|
+
coerce: single("project-id")
|
|
57
|
+
},
|
|
58
|
+
force: {
|
|
59
|
+
describe: "Replace an existing profile, revoking the credential it holds now",
|
|
60
|
+
type: "boolean",
|
|
61
|
+
default: false
|
|
62
|
+
}
|
|
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", {
|
|
64
|
+
describe: "Profile to rotate",
|
|
65
|
+
type: "string",
|
|
66
|
+
demandOption: true
|
|
67
|
+
}).strict().check(noPassthrough("profile rotate-key")), async (args) => await rotateKey(args)).command("remove <name>", "Revoke a profile's credential where possible, and remove it", (y) => y.positional("name", {
|
|
68
|
+
describe: "Profile to remove",
|
|
69
|
+
type: "string",
|
|
70
|
+
demandOption: true
|
|
71
|
+
}).option("yes", {
|
|
72
|
+
alias: "y",
|
|
73
|
+
describe: "Skip the confirmation prompt",
|
|
74
|
+
type: "boolean",
|
|
75
|
+
default: false
|
|
76
|
+
}).strict().check(noPassthrough("profile remove")), async (args) => await remove(args)).demandCommand(1, "Run `neon profile --help` to see the subcommands.");
|
|
77
|
+
const handler = (_args) => {};
|
|
100
78
|
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
79
|
+
* Refuse a global credential flag that a `profile` subcommand cannot honour.
|
|
80
|
+
*
|
|
81
|
+
* These commands name their target as a positional and authenticate as that profile, so both
|
|
82
|
+
* `--profile` and `--api-key` have nothing to select here and were silently dropped — which is
|
|
83
|
+
* precisely the class of bug this command family exists to stop. `.strict()` cannot catch
|
|
84
|
+
* either, because both are global options and yargs has already accepted them.
|
|
85
|
+
*
|
|
86
|
+
* No "did you mean". The suggestion this used to make was built from the flag rather than the
|
|
87
|
+
* positional, so it renamed the target: `profile remove work --profile other` answered "did you
|
|
88
|
+
* mean `neon profile remove other`". Since the name is a required positional, that was wrong in
|
|
89
|
+
* every case it fired, and on `remove` it was copy-pasteable destructive advice for an account
|
|
90
|
+
* the user had not mentioned.
|
|
91
|
+
*/
|
|
114
92
|
const rejectProfileFlag = (props, sub) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
throw new Error(`--profile does not apply to \`profile ${sub}\`, which takes the profile name as an argument. You passed both "${props.name}" and --profile ${named}; drop --profile.`);
|
|
93
|
+
const named = props.profile?.trim();
|
|
94
|
+
if (!named) return;
|
|
95
|
+
throw new Error(`--profile does not apply to \`profile ${sub}\`, which takes the profile name as an argument. You passed both "${props.name}" and --profile ${named}; drop --profile.`);
|
|
119
96
|
};
|
|
120
97
|
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
* Refuse `--api-key` on a subcommand that authenticates as the profile it was given.
|
|
99
|
+
*
|
|
100
|
+
* The mirror image of the flag above, and it was the one left silent: `profile remove work
|
|
101
|
+
* --api-key napi_…` ignored the key, used the stored one, and reported a revoke failure against
|
|
102
|
+
* a credential the user had not passed. This PR exists because a dropped credential flag chose
|
|
103
|
+
* the wrong account; leaving that unsaid on the commands that manage credentials would be the
|
|
104
|
+
* same bug with the arguments swapped.
|
|
105
|
+
*/
|
|
129
106
|
const rejectApiKeyFlag = (sub, instead) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new Error(`--api-key does not apply to \`profile ${sub}\`, which uses the credential the profile already holds.${instead ? ` ${instead}` : ""}`);
|
|
107
|
+
if (credentialInputs().apiKeyFlag.trim() === "") return;
|
|
108
|
+
throw new Error(`--api-key does not apply to \`profile ${sub}\`, which uses the credential the profile already holds.${instead ? ` ${instead}` : ""}`);
|
|
133
109
|
};
|
|
134
110
|
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
111
|
+
* What kind of credential a file holds, for display.
|
|
112
|
+
*
|
|
113
|
+
* A file that declares a kind we cannot read is reported as `invalid` with the reason logged,
|
|
114
|
+
* rather than throwing: one broken profile must not hide every other row, and the whole point
|
|
115
|
+
* of `list` is to show the state of things including the broken parts.
|
|
116
|
+
*/
|
|
141
117
|
const describeAuth = (stored, at) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
? "api key"
|
|
150
|
-
: "oauth";
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
log.warning(err instanceof Error ? err.message : String(err));
|
|
154
|
-
return "invalid";
|
|
155
|
-
}
|
|
118
|
+
if (stored === null) return "-";
|
|
119
|
+
try {
|
|
120
|
+
return interpretCredentials(stored, at).kind === "api_key" ? "api key" : "oauth";
|
|
121
|
+
} catch (err) {
|
|
122
|
+
log.warning(err instanceof Error ? err.message : String(err));
|
|
123
|
+
return "invalid";
|
|
124
|
+
}
|
|
156
125
|
};
|
|
157
126
|
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
127
|
+
* One row's view of a credentials file.
|
|
128
|
+
*
|
|
129
|
+
* Reports a damaged file as `invalid` and carries on, because one broken profile must not take
|
|
130
|
+
* the whole listing with it — and unlike the authenticating path, listing has nothing to
|
|
131
|
+
* recover, so the row itself is the report.
|
|
132
|
+
*
|
|
133
|
+
* One column answers "can this be used". A file whose JSON parses but whose `type` is
|
|
134
|
+
* unreadable used to show `File: ok` beside `Auth: invalid`, so the column a user scans to find
|
|
135
|
+
* the broken profile pointed at a different row than the one that was broken.
|
|
136
|
+
*/
|
|
168
137
|
const inspectForListing = (at) => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 {
|
|
148
|
+
stored: null,
|
|
149
|
+
auth: "-",
|
|
150
|
+
file: "missing"
|
|
151
|
+
};
|
|
152
|
+
const auth = describeAuth(read.credentials, at);
|
|
153
|
+
return {
|
|
154
|
+
stored: read.credentials,
|
|
155
|
+
auth,
|
|
156
|
+
file: auth === "invalid" ? "invalid" : "ok"
|
|
157
|
+
};
|
|
182
158
|
};
|
|
183
159
|
const list = async (props) => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
? { credentials: basename(p.credentialsPath) }
|
|
216
|
-
: { credentials: p.credentialsPath }),
|
|
217
|
-
};
|
|
218
|
-
});
|
|
219
|
-
writer(props).end(rows, {
|
|
220
|
-
title: "Profiles",
|
|
221
|
-
fields: [
|
|
222
|
-
"active",
|
|
223
|
-
"name",
|
|
224
|
-
"account",
|
|
225
|
-
"auth",
|
|
226
|
-
"scope",
|
|
227
|
-
"file",
|
|
228
|
-
"credentials",
|
|
229
|
-
],
|
|
230
|
-
});
|
|
160
|
+
rejectApiKeyFlag("list");
|
|
161
|
+
const active = selectProfileName(props.profile);
|
|
162
|
+
if (props.profile !== void 0 && props.profile.trim() !== "") resolveProfile(props.configDir, active);
|
|
163
|
+
const rows = listProfiles(props.configDir).map((p) => {
|
|
164
|
+
const { stored, auth, file } = inspectForListing({
|
|
165
|
+
path: p.credentialsPath,
|
|
166
|
+
profile: p.name
|
|
167
|
+
});
|
|
168
|
+
const storedUserId = typeof stored?.user_id === "string" ? stored.user_id : void 0;
|
|
169
|
+
return {
|
|
170
|
+
active: p.name === active ? "*" : "",
|
|
171
|
+
name: p.name,
|
|
172
|
+
account: p.label ?? p.userId ?? storedUserId ?? "-",
|
|
173
|
+
auth,
|
|
174
|
+
scope: stored !== null && auth === "api key" ? describeScope(scopeOf(stored)) : "-",
|
|
175
|
+
file,
|
|
176
|
+
...props.output === "table" ? { credentials: basename(p.credentialsPath) } : { credentials: p.credentialsPath }
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
writer(props).end(rows, {
|
|
180
|
+
title: "Profiles",
|
|
181
|
+
fields: [
|
|
182
|
+
"active",
|
|
183
|
+
"name",
|
|
184
|
+
"account",
|
|
185
|
+
"auth",
|
|
186
|
+
"scope",
|
|
187
|
+
"file",
|
|
188
|
+
"credentials"
|
|
189
|
+
]
|
|
190
|
+
});
|
|
231
191
|
};
|
|
232
192
|
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
*/
|
|
240
200
|
const readOutgoingCredential = (at) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
kind: API_KEY,
|
|
264
|
-
apiKey: credential.apiKey,
|
|
265
|
-
...(typeof read.credentials.key_id === "number"
|
|
266
|
-
? { keyId: read.credentials.key_id }
|
|
267
|
-
: {}),
|
|
268
|
-
scope: scopeOf(read.credentials),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
catch (err) {
|
|
272
|
-
return unusable(err instanceof Error ? err.message : String(err));
|
|
273
|
-
}
|
|
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;
|
|
205
|
+
};
|
|
206
|
+
if (read.kind === "unusable") return unusable(read.reason);
|
|
207
|
+
if (read.kind === "absent") return null;
|
|
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));
|
|
222
|
+
}
|
|
274
223
|
};
|
|
275
224
|
const credentialsPathFor = (configDir, name) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// before `upsertProfile` refused the same metadata.
|
|
280
|
-
assertProfilesUsable(configDir, name);
|
|
281
|
-
if (readProfiles(configDir)?.profiles[name] || name === DEFAULT_PROFILE) {
|
|
282
|
-
return credentialsPathForName(configDir, name);
|
|
283
|
-
}
|
|
284
|
-
return newProfileCredentialsPath(configDir, name);
|
|
225
|
+
assertProfilesUsable(configDir, name);
|
|
226
|
+
if (readProfiles(configDir)?.profiles[name] || name === "DEFAULT") return credentialsPathForName(configDir, name);
|
|
227
|
+
return newProfileCredentialsPath(configDir, name);
|
|
285
228
|
};
|
|
286
229
|
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
*/
|
|
295
238
|
const assertReplaceable = (props) => {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (holdsKey) {
|
|
311
|
-
throw new Error(keyId !== undefined
|
|
312
|
-
? `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}\`.`
|
|
313
|
-
: `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.`);
|
|
314
|
-
}
|
|
315
|
-
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.`);
|
|
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.`);
|
|
316
253
|
};
|
|
317
254
|
/** `--api-key -` means "read it from stdin", the usual convention for a piped value. */
|
|
318
255
|
const STDIN = "-";
|
|
319
256
|
/**
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
257
|
+
* The key to store.
|
|
258
|
+
*
|
|
259
|
+
* Read from the *flag* and never from `NEON_API_KEY`. Everywhere else those are
|
|
260
|
+
* interchangeable, but "store this credential permanently" is not something an exported
|
|
261
|
+
* environment variable should be able to answer — a `create` that silently wrote whatever
|
|
262
|
+
* happened to be in the shell would be storing a key the user never named.
|
|
263
|
+
*
|
|
264
|
+
* One flag, deliberately. Earlier revisions added `--api-key-file`, `--api-key-stdin` and
|
|
265
|
+
* `--api-key-prompt` to keep the secret out of argv, which is four surfaces to document and
|
|
266
|
+
* validate against each other for things the shell already does — `--api-key "$(cat file)"`
|
|
267
|
+
* reads a file, `--api-key "$KEY"` takes a variable. Piping is the one case the shell cannot
|
|
268
|
+
* express through an argument, so it gets the usual convention instead of a flag: `-`.
|
|
269
|
+
*/
|
|
333
270
|
const resolveKeyToStore = (props) => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return fromFlag;
|
|
339
|
-
throw new Error(`Nothing to store for profile "${props.name}". Pass --api-key (or --api-key - to pipe it), --mint to have one minted, or no flags at all to sign in with the browser.`);
|
|
271
|
+
const fromFlag = credentialInputs().apiKeyFlag.trim();
|
|
272
|
+
if (fromFlag === STDIN) return readKeyFromStdin();
|
|
273
|
+
if (fromFlag !== "") return fromFlag;
|
|
274
|
+
throw new Error(`Nothing to store for profile "${props.name}". Pass --api-key (or --api-key - to pipe it), --mint to have one minted, or no flags at all to sign in with the browser.`);
|
|
340
275
|
};
|
|
341
276
|
/**
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
277
|
+
* Read the key from stdin, for `--api-key -`.
|
|
278
|
+
*
|
|
279
|
+
* A terminal is refused rather than read: `readFileSync(0)` on a tty waits for EOF with no
|
|
280
|
+
* prompt and nothing echoed, which is indistinguishable from a hang — and this spelling exists
|
|
281
|
+
* for pipes, so a terminal means the pipe is missing.
|
|
282
|
+
*/
|
|
348
283
|
const readKeyFromStdin = () => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (piped === "") {
|
|
354
|
-
throw new Error("Nothing arrived on stdin, so there is no API key to store.");
|
|
355
|
-
}
|
|
356
|
-
return piped;
|
|
284
|
+
if (process.stdin.isTTY) throw new Error("`--api-key -` reads the key from stdin, but stdin is a terminal. Pipe it in: echo \"$KEY\" | neon profile create <name> --api-key -");
|
|
285
|
+
const piped = readFileSync(0, "utf8").trim();
|
|
286
|
+
if (piped === "") throw new Error("Nothing arrived on stdin, so there is no API key to store.");
|
|
287
|
+
return piped;
|
|
357
288
|
};
|
|
358
289
|
/**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const verifyKey = async (props, apiKey,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
throw err;
|
|
381
|
-
});
|
|
382
|
-
if (!isApiKeyMethod(details.auth_method)) {
|
|
383
|
-
throw new Error(notAnApiKeyMessage(details.auth_method));
|
|
384
|
-
}
|
|
385
|
-
// Only a user key has a user to look up: `GET /users/me` answers 404 for an organization
|
|
386
|
-
// key, so asking anyway would fail a `create` that is working perfectly.
|
|
387
|
-
const email = details.auth_method === "api_key_user"
|
|
388
|
-
? (await apiClient.getCurrentUserInfo()).data.email
|
|
389
|
-
: undefined;
|
|
390
|
-
return {
|
|
391
|
-
...identityFromAuthDetails(details, email),
|
|
392
|
-
...(details.auth_method === "api_key_org"
|
|
393
|
-
? { orgId: details.account_id }
|
|
394
|
-
: {}),
|
|
395
|
-
apiClient,
|
|
396
|
-
};
|
|
290
|
+
* Confirm a key works and find out whose it is, before it is written anywhere.
|
|
291
|
+
*
|
|
292
|
+
* `getAuthDetails` rather than `GET /users/me`: an organization key gets `404 not allowed for
|
|
293
|
+
* organization API keys` from the latter, so asking would fail a perfectly good key.
|
|
294
|
+
*/
|
|
295
|
+
const verifyKey = async (props, apiKey, origin = "supplied") => {
|
|
296
|
+
const apiClient = getApiClient({
|
|
297
|
+
apiKey,
|
|
298
|
+
apiHost: props.apiHost
|
|
299
|
+
});
|
|
300
|
+
const { data: details } = await apiClient.getAuthDetails().catch((err) => {
|
|
301
|
+
if (isNeonApiError(err) && err.status === 401) throw new Error(origin === "minted" ? "Neon rejected the key it had just minted. Nothing was stored; the key is being revoked." : "The Neon API rejected the key passed to --api-key. Check the value, or use --mint to have one minted for this profile.");
|
|
302
|
+
throw err;
|
|
303
|
+
});
|
|
304
|
+
if (!isApiKeyMethod(details.auth_method)) throw new Error(notAnApiKeyMessage(details.auth_method));
|
|
305
|
+
const email = details.auth_method === "api_key_user" ? (await apiClient.getCurrentUserInfo()).data.email : void 0;
|
|
306
|
+
return {
|
|
307
|
+
...identityFromAuthDetails(details, email),
|
|
308
|
+
...details.auth_method === "api_key_org" ? { orgId: details.account_id } : {},
|
|
309
|
+
apiClient
|
|
310
|
+
};
|
|
397
311
|
};
|
|
398
312
|
const create = async (props) => {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
apiKey,
|
|
457
|
-
...(identity.userId !== undefined
|
|
458
|
-
? { userId: identity.userId }
|
|
459
|
-
: {}),
|
|
460
|
-
// A supplied organization key does not say which project it reaches, but the API
|
|
461
|
-
// does say which organization — recording it keeps `list` from claiming "account",
|
|
462
|
-
// and lets a later rotation refuse on the right grounds.
|
|
463
|
-
...(identity.orgId !== undefined
|
|
464
|
-
? { scope: { orgId: identity.orgId } }
|
|
465
|
-
: {}),
|
|
466
|
-
}));
|
|
467
|
-
await retirePreviousCredential(props, name, previous, apiKey);
|
|
468
|
-
recordProfile(props, name, credentialsPath, identity);
|
|
469
|
-
report(props, {
|
|
470
|
-
name,
|
|
471
|
-
account: identity.label ?? "unknown account",
|
|
472
|
-
auth: "api key",
|
|
473
|
-
scope: describeScope(identity.orgId !== undefined ? { orgId: identity.orgId } : {}),
|
|
474
|
-
credentials: credentialsPath,
|
|
475
|
-
});
|
|
476
|
-
// A key we did not mint has no discoverable id: `GET /api_keys` exposes no prefix, so a
|
|
477
|
-
// stored secret cannot be matched back to a listing entry. A warning rather than a note,
|
|
478
|
-
// because it is the same caveat `api-keys create` raises at that level.
|
|
479
|
-
log.warning("A key you supplied cannot be revoked by `rotate-key` or `remove` — only keys minted here record an id.");
|
|
313
|
+
const { name } = props;
|
|
314
|
+
rejectProfileFlag(props, "create");
|
|
315
|
+
assertValidProfileName(name);
|
|
316
|
+
assertProfilesUsable(props.configDir, name);
|
|
317
|
+
assertReplaceable(props);
|
|
318
|
+
const suppliedKey = credentialInputs().apiKeyFlag.trim() !== "";
|
|
319
|
+
if (props.mint) {
|
|
320
|
+
if (suppliedKey) throw new Error("--mint creates a new key, so it cannot be combined with --api-key. Drop one.");
|
|
321
|
+
await createByMinting(props);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
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
|
+
if (!suppliedKey) {
|
|
326
|
+
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
|
+
});
|
|
332
|
+
if (await authFlow({
|
|
333
|
+
...props,
|
|
334
|
+
_: ["auth"],
|
|
335
|
+
profile: name
|
|
336
|
+
}) === "") throw new Error(`Could not save credentials for profile "${name}".`);
|
|
337
|
+
await retirePreviousCredential(props, name, previous);
|
|
338
|
+
const signedIn = readProfiles(props.configDir, log.warning)?.profiles[name];
|
|
339
|
+
report(props, {
|
|
340
|
+
name,
|
|
341
|
+
account: signedIn?.label ?? signedIn?.userId ?? "unknown account",
|
|
342
|
+
auth: "oauth",
|
|
343
|
+
scope: "-",
|
|
344
|
+
credentials: credentialsPath
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const apiKey = resolveKeyToStore(props);
|
|
349
|
+
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({
|
|
356
|
+
apiKey,
|
|
357
|
+
...identity.userId !== void 0 ? { userId: identity.userId } : {},
|
|
358
|
+
...identity.orgId !== void 0 ? { scope: { orgId: identity.orgId } } : {}
|
|
359
|
+
}));
|
|
360
|
+
await retirePreviousCredential(props, name, previous, apiKey);
|
|
361
|
+
recordProfile(props, name, credentialsPath, identity);
|
|
362
|
+
report(props, {
|
|
363
|
+
name,
|
|
364
|
+
account: identity.label ?? "unknown account",
|
|
365
|
+
auth: "api key",
|
|
366
|
+
scope: describeScope(identity.orgId !== void 0 ? { orgId: identity.orgId } : {}),
|
|
367
|
+
credentials: credentialsPath
|
|
368
|
+
});
|
|
369
|
+
log.warning("A key you supplied cannot be revoked by `rotate-key` or `remove` — only keys minted here record an id.");
|
|
480
370
|
};
|
|
481
371
|
/**
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
372
|
+
* What a minted key can reach, in the same words `neon api-keys create` uses for the same key.
|
|
373
|
+
*
|
|
374
|
+
* Saying nothing here would be worse than there: this is the command aimed at agents and shared
|
|
375
|
+
* machines, which is exactly where an over-broad credential does the most damage.
|
|
376
|
+
*/
|
|
487
377
|
const warnAboutReach = (scope) => {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
378
|
+
if (scope.projectId !== void 0) {
|
|
379
|
+
log.info("Limited to %s: it cannot create projects, mint API keys, or read any other project. It can still change and delete everything inside that project.", scope.projectId);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (scope.orgId !== void 0) {
|
|
383
|
+
log.warning("This key reaches every project in %s, including ones created later. Pass --project-id instead to restrict it to one.", scope.orgId);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
log.warning("This key reaches everything your account can, in every organization. Pass --org-id or --project-id to narrow it.");
|
|
497
387
|
};
|
|
498
388
|
/**
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
389
|
+
* Report a profile that was just written.
|
|
390
|
+
*
|
|
391
|
+
* Through the writer rather than `log`, so `--output json` yields the record an agent needs
|
|
392
|
+
* instead of nothing — it had to follow up with `profile list` to learn what it just created.
|
|
393
|
+
*/
|
|
504
394
|
const report = (props, record) => {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
395
|
+
const out = writer(props);
|
|
396
|
+
if (props.output === "table") {
|
|
397
|
+
log.info("Profile \"%s\" now holds %s for %s (%s), in %s", record.name, record.auth, record.account, record.scope, record.credentials);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
out.end(record, {
|
|
401
|
+
fields: [
|
|
402
|
+
"name",
|
|
403
|
+
"account",
|
|
404
|
+
"auth",
|
|
405
|
+
"scope",
|
|
406
|
+
...record.keyId !== void 0 ? ["keyId"] : [],
|
|
407
|
+
"credentials"
|
|
408
|
+
],
|
|
409
|
+
title: "Profile"
|
|
410
|
+
});
|
|
521
411
|
};
|
|
522
412
|
const recordProfile = (props, name, credentialsPath, identity) => {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
413
|
+
upsertProfile(props.configDir, name, {
|
|
414
|
+
credentials: credentialsPath,
|
|
415
|
+
...identity.label ? { label: identity.label } : {},
|
|
416
|
+
...identity.userId ? { userId: identity.userId } : {}
|
|
417
|
+
});
|
|
528
418
|
};
|
|
529
419
|
/**
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
420
|
+
* Sign in once, mint a key, keep only the key.
|
|
421
|
+
*
|
|
422
|
+
* This is the reason to prefer a key-backed profile at all: after it, nothing about the profile
|
|
423
|
+
* can trigger a browser. The OAuth session is a means to an end and is revoked at the end of
|
|
424
|
+
* it, so no half-forgotten login is left behind — which also means the file holds exactly one
|
|
425
|
+
* credential, for exactly one account.
|
|
426
|
+
*/
|
|
537
427
|
const createByMinting = async (props) => {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
// failure writing `profiles.json` costs a profile entry, not the credential.
|
|
587
|
-
keyIsReachable = true;
|
|
588
|
-
await retirePreviousCredential(props, name, previous, minted.key);
|
|
589
|
-
recordProfile(props, name, credentialsPath, identity);
|
|
590
|
-
report(props, {
|
|
591
|
-
name,
|
|
592
|
-
account: identity.label ?? "unknown account",
|
|
593
|
-
auth: "api key",
|
|
594
|
-
scope: describeScope(scope),
|
|
595
|
-
keyId: minted.id,
|
|
596
|
-
credentials: credentialsPath,
|
|
597
|
-
});
|
|
598
|
-
warnAboutReach(scope);
|
|
599
|
-
}
|
|
600
|
-
finally {
|
|
601
|
-
// A key minted but never written is unreachable, so it must not be left live.
|
|
602
|
-
if (minted !== undefined && !keyIsReachable) {
|
|
603
|
-
log.info((await withdrawKey(session, mintedScope, minted.id))
|
|
604
|
-
? `Revoked the key that was minted but not stored (id ${minted.id}).`
|
|
605
|
-
: `Minted key ${minted.id} could be neither stored nor revoked, and may still be live. Remove it with: neon api-keys revoke ${minted.id}${mintedScope.orgId ? ` --org-id ${mintedScope.orgId}` : ""}`);
|
|
606
|
-
}
|
|
607
|
-
const revoked = await revokeToken(oauthProps, tokenSet);
|
|
608
|
-
log.info(revoked
|
|
609
|
-
? "Signed the browser session back out, so nothing but the key remains."
|
|
610
|
-
: "Could not sign the browser session back out; it will expire on its own.");
|
|
611
|
-
}
|
|
428
|
+
const { name } = props;
|
|
429
|
+
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 -`);
|
|
430
|
+
const oauthProps = {
|
|
431
|
+
oauthHost: props.oauthHost,
|
|
432
|
+
clientId: props.clientId,
|
|
433
|
+
...props.allowUnsafeTls ? { allowUnsafeTls: props.allowUnsafeTls } : {}
|
|
434
|
+
};
|
|
435
|
+
const tokenSet = await auth(oauthProps);
|
|
436
|
+
const session = getApiClient({
|
|
437
|
+
apiKey: tokenSet.access_token ?? "",
|
|
438
|
+
apiHost: props.apiHost
|
|
439
|
+
});
|
|
440
|
+
let minted;
|
|
441
|
+
let mintedScope = {};
|
|
442
|
+
let keyIsReachable = false;
|
|
443
|
+
try {
|
|
444
|
+
const scope = await resolveMintScope(props, session);
|
|
445
|
+
mintedScope = scope;
|
|
446
|
+
minted = await mintKey(session, name, scope);
|
|
447
|
+
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({
|
|
454
|
+
apiKey: minted.key,
|
|
455
|
+
keyId: minted.id,
|
|
456
|
+
...identity.userId !== void 0 ? { userId: identity.userId } : {},
|
|
457
|
+
scope
|
|
458
|
+
}));
|
|
459
|
+
keyIsReachable = true;
|
|
460
|
+
await retirePreviousCredential(props, name, previous, minted.key);
|
|
461
|
+
recordProfile(props, name, credentialsPath, identity);
|
|
462
|
+
report(props, {
|
|
463
|
+
name,
|
|
464
|
+
account: identity.label ?? "unknown account",
|
|
465
|
+
auth: "api key",
|
|
466
|
+
scope: describeScope(scope),
|
|
467
|
+
keyId: minted.id,
|
|
468
|
+
credentials: credentialsPath
|
|
469
|
+
});
|
|
470
|
+
warnAboutReach(scope);
|
|
471
|
+
} finally {
|
|
472
|
+
if (minted !== void 0 && !keyIsReachable) log.info(await withdrawKey(session, mintedScope, minted.id) ? `Revoked the key that was minted but not stored (id ${minted.id}).` : `Minted key ${minted.id} could be neither stored nor revoked, and may still be live. Remove it with: neon api-keys revoke ${minted.id}${mintedScope.orgId ? ` --org-id ${mintedScope.orgId}` : ""}`);
|
|
473
|
+
const revoked = await revokeToken(oauthProps, tokenSet);
|
|
474
|
+
log.info(revoked ? "Signed the browser session back out, so nothing but the key remains." : "Could not sign the browser session back out; it will expire on its own.");
|
|
475
|
+
}
|
|
612
476
|
};
|
|
613
477
|
/**
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
478
|
+
* The scope to mint at, resolving a project to the organization that owns it.
|
|
479
|
+
*
|
|
480
|
+
* A project-scoped key exists only on the organization endpoint, so the org has to be looked up
|
|
481
|
+
* rather than asked for — `--project-id` alone would otherwise fail for a reason invisible from
|
|
482
|
+
* the command line.
|
|
483
|
+
*/
|
|
620
484
|
const resolveMintScope = async (props, client) => {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// same typo got a written explanation there and a raw 404 here.
|
|
627
|
-
return {
|
|
628
|
-
orgId: await orgIdForProject(client, props.projectId),
|
|
629
|
-
projectId: props.projectId,
|
|
630
|
-
};
|
|
485
|
+
if (props.projectId === void 0) return props.orgId !== void 0 ? { orgId: props.orgId } : {};
|
|
486
|
+
return {
|
|
487
|
+
orgId: await orgIdForProject(client, props.projectId),
|
|
488
|
+
projectId: props.projectId
|
|
489
|
+
};
|
|
631
490
|
};
|
|
632
491
|
/**
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const retirePreviousCredential = async (props, name, existing,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
log.info((await withdrawKey(client, existing.scope, existing.keyId))
|
|
662
|
-
? `Revoked the key it replaces (id ${existing.keyId})`
|
|
663
|
-
: `Could not revoke the key it replaces (id ${existing.keyId}); it may still be live. Remove it with: neon api-keys revoke ${existing.keyId}`);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
const revoked = await revokeTokenSet(existing.tokens, props);
|
|
667
|
-
log.info(revoked
|
|
668
|
-
? "Signed out the session it replaced"
|
|
669
|
-
: "Could not sign out the session it replaced; it will expire on its own");
|
|
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");
|
|
670
520
|
};
|
|
671
521
|
/**
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
522
|
+
* Mint a key at the given scope and refuse anything that came back different.
|
|
523
|
+
*
|
|
524
|
+
* A 2xx is not enough. A response with no `key` leaves a live credential the user can never
|
|
525
|
+
* see, and a `project_id` that does not match would mean storing a credential whose reach we
|
|
526
|
+
* announced wrongly. Both are withdrawn before throwing.
|
|
527
|
+
*/
|
|
678
528
|
const mintKey = async (client, profile, scope) => {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
? "Neon returned no key."
|
|
694
|
-
: issuedProject !== scope.projectId
|
|
695
|
-
? `Neon returned a key scoped to ${issuedProject ?? "nothing"} rather than ${scope.projectId ?? "the whole organization"}.`
|
|
696
|
-
: null;
|
|
697
|
-
if (problem === null) {
|
|
698
|
-
return { id: data.id, key: data.key, name: data.name };
|
|
699
|
-
}
|
|
700
|
-
const withdrawn = await withdrawKey(client, scope, data.id);
|
|
701
|
-
throw new Error(`${problem} ${withdrawn
|
|
702
|
-
? "The key has been revoked; nothing was stored."
|
|
703
|
-
: `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}` : ""}\`.`}`);
|
|
529
|
+
const keyName = mintedKeyName(profile);
|
|
530
|
+
const { data } = scope.orgId ? await client.createOrgApiKey(scope.orgId, {
|
|
531
|
+
key_name: keyName,
|
|
532
|
+
...scope.projectId ? { project_id: scope.projectId } : {}
|
|
533
|
+
}) : await client.createApiKey({ key_name: keyName });
|
|
534
|
+
const issuedProject = "project_id" in data && typeof data.project_id === "string" ? data.project_id : void 0;
|
|
535
|
+
const problem = typeof data.key !== "string" || data.key.trim() === "" ? "Neon returned no key." : issuedProject !== scope.projectId ? `Neon returned a key scoped to ${issuedProject ?? "nothing"} rather than ${scope.projectId ?? "the whole organization"}.` : null;
|
|
536
|
+
if (problem === null) return {
|
|
537
|
+
id: data.id,
|
|
538
|
+
key: data.key,
|
|
539
|
+
name: data.name
|
|
540
|
+
};
|
|
541
|
+
const withdrawn = await withdrawKey(client, scope, data.id);
|
|
542
|
+
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}` : ""}\`.`}`);
|
|
704
543
|
};
|
|
705
544
|
/** Best-effort withdrawal of a key we are refusing to store. Never throws. */
|
|
706
545
|
const withdrawKey = async (client, scope, keyId) => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
return data.revoked === true && data.id === keyId;
|
|
716
|
-
}
|
|
717
|
-
catch (err) {
|
|
718
|
-
log.error("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
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
|
+
}
|
|
721
554
|
};
|
|
722
555
|
const rotateKey = async (props) => {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}));
|
|
787
|
-
}
|
|
788
|
-
catch (err) {
|
|
789
|
-
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}` : "");
|
|
790
|
-
throw err;
|
|
791
|
-
}
|
|
792
|
-
report(props, {
|
|
793
|
-
name,
|
|
794
|
-
account: profile.label ?? profile.userId ?? "unknown account",
|
|
795
|
-
auth: "api key",
|
|
796
|
-
scope: describeScope(scope),
|
|
797
|
-
keyId: created.id,
|
|
798
|
-
credentials: profile.credentialsPath,
|
|
799
|
-
});
|
|
800
|
-
if (previousKeyId === undefined) {
|
|
801
|
-
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`.");
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
// Revoke with the new key. The old one may be exactly what stopped working, which is the
|
|
805
|
-
// usual reason to rotate in the first place.
|
|
806
|
-
const rotated = getApiClient({
|
|
807
|
-
apiKey: created.key,
|
|
808
|
-
apiHost: props.apiHost,
|
|
809
|
-
});
|
|
810
|
-
if (await withdrawKey(rotated, scope, previousKeyId)) {
|
|
811
|
-
log.info("Revoked the previous key (id %d)", previousKeyId);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
log.warning("Stored the new key, but could not revoke the previous one (id %d) — it may still be live. Revoke it with: neon api-keys revoke %d%s", previousKeyId, previousKeyId, scope.orgId ? ` --org-id ${scope.orgId}` : "");
|
|
556
|
+
const { name } = props;
|
|
557
|
+
rejectProfileFlag(props, "rotate-key");
|
|
558
|
+
rejectApiKeyFlag("rotate-key", `To store a key you already have, use \`neon profile create ${name} --api-key - --force\`.`);
|
|
559
|
+
const profile = resolveProfile(props.configDir, name);
|
|
560
|
+
const at = {
|
|
561
|
+
path: profile.credentialsPath,
|
|
562
|
+
profile: name
|
|
563
|
+
};
|
|
564
|
+
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);
|
|
568
|
+
const scope = stored !== null ? scopeOf(stored) : {};
|
|
569
|
+
const previousKeyId = typeof stored?.key_id === "number" ? stored.key_id : void 0;
|
|
570
|
+
const minting = getApiClient({
|
|
571
|
+
apiKey: credential.apiKey,
|
|
572
|
+
apiHost: props.apiHost
|
|
573
|
+
});
|
|
574
|
+
setAuthContext({
|
|
575
|
+
source: "profile-api-key",
|
|
576
|
+
configDir: props.configDir,
|
|
577
|
+
profile: name,
|
|
578
|
+
credentialsPath: profile.credentialsPath
|
|
579
|
+
});
|
|
580
|
+
const { data: details } = await minting.getAuthDetails();
|
|
581
|
+
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`;
|
|
583
|
+
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
|
+
}
|
|
585
|
+
const created = await mintKey(minting, name, scope);
|
|
586
|
+
log.info("Minted %s (id %d, %s)", created.name, created.id, describeScope(scope));
|
|
587
|
+
try {
|
|
588
|
+
writeCredentials(profile.credentialsPath, apiKeyCredentials({
|
|
589
|
+
apiKey: created.key,
|
|
590
|
+
keyId: created.id,
|
|
591
|
+
...typeof stored?.user_id === "string" ? { userId: stored.user_id } : {},
|
|
592
|
+
scope
|
|
593
|
+
}));
|
|
594
|
+
} 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}` : "");
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
report(props, {
|
|
599
|
+
name,
|
|
600
|
+
account: profile.label ?? profile.userId ?? "unknown account",
|
|
601
|
+
auth: "api key",
|
|
602
|
+
scope: describeScope(scope),
|
|
603
|
+
keyId: created.id,
|
|
604
|
+
credentials: profile.credentialsPath
|
|
605
|
+
});
|
|
606
|
+
if (previousKeyId === void 0) {
|
|
607
|
+
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`.");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const rotated = getApiClient({
|
|
611
|
+
apiKey: created.key,
|
|
612
|
+
apiHost: props.apiHost
|
|
613
|
+
});
|
|
614
|
+
if (await withdrawKey(rotated, scope, previousKeyId)) {
|
|
615
|
+
log.info("Revoked the previous key (id %d)", previousKeyId);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
log.warning("Stored the new key, but could not revoke the previous one (id %d) — it may still be live. Revoke it with: neon api-keys revoke %d%s", previousKeyId, previousKeyId, scope.orgId ? ` --org-id ${scope.orgId}` : "");
|
|
815
619
|
};
|
|
816
620
|
const remove = async (props) => {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const revoked = await revokeTokenSet(stored.tokens, props);
|
|
868
|
-
log.info(revoked
|
|
869
|
-
? "Revoked the OAuth token"
|
|
870
|
-
: "Could not revoke the OAuth token — removing locally anyway");
|
|
871
|
-
}
|
|
872
|
-
// 2. Delete the credentials file only if we created it. A profile pointing outside the
|
|
873
|
-
// config directory was adopted from elsewhere; unlink it and say so, because the
|
|
874
|
-
// secret is still on disk and silence would imply otherwise.
|
|
875
|
-
if (existsSync(profile.credentialsPath)) {
|
|
876
|
-
if (isInsideConfigDir(props.configDir, profile.credentialsPath)) {
|
|
877
|
-
rmSync(profile.credentialsPath);
|
|
878
|
-
log.info("Deleted %s", profile.credentialsPath);
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
log.info("Left %s on disk — not created by neon", profile.credentialsPath);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
// 3. Drop the entry, and the file once nothing but DEFAULT is left — the mirror image
|
|
885
|
-
// of creating it lazily, so a single-account install ends up with no profiles.json.
|
|
886
|
-
const path = profilesFilePath(props.configDir);
|
|
887
|
-
const file = readProfiles(props.configDir, log.warning);
|
|
888
|
-
if (file?.profiles[name]) {
|
|
889
|
-
delete file.profiles[name];
|
|
890
|
-
if (onlyDefaultRemains(file)) {
|
|
891
|
-
rmSync(path);
|
|
892
|
-
log.info('Removed "%s" — no profiles left, deleted %s', name, path);
|
|
893
|
-
}
|
|
894
|
-
else {
|
|
895
|
-
writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
896
|
-
log.info('Removed "%s" from %s', name, path);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
else if (name === DEFAULT_PROFILE) {
|
|
900
|
-
log.info("Signed out of DEFAULT");
|
|
901
|
-
}
|
|
621
|
+
const { name } = props;
|
|
622
|
+
rejectProfileFlag(props, "remove");
|
|
623
|
+
rejectApiKeyFlag("remove", "It revokes the credential stored for that profile, which is the only one it can revoke.");
|
|
624
|
+
const profile = resolveProfile(props.configDir, name);
|
|
625
|
+
if (!props.yes) {
|
|
626
|
+
if (isCi() || !process.stdin.isTTY) throw new Error("Refusing to remove a profile without confirmation when stdin is not a terminal. Pass --yes.");
|
|
627
|
+
const who = profile.label ?? profile.userId ?? "unknown account";
|
|
628
|
+
const { ok } = await prompts({
|
|
629
|
+
type: "confirm",
|
|
630
|
+
name: "ok",
|
|
631
|
+
message: `Remove profile "${name}" (${who})?`,
|
|
632
|
+
initial: false
|
|
633
|
+
});
|
|
634
|
+
if (!ok) throw new Error(`Cancelled — profile "${name}" was not removed.`);
|
|
635
|
+
}
|
|
636
|
+
const at = {
|
|
637
|
+
path: profile.credentialsPath,
|
|
638
|
+
profile: name
|
|
639
|
+
};
|
|
640
|
+
const stored = readOutgoingCredential(at);
|
|
641
|
+
if (stored?.kind === "api_key") {
|
|
642
|
+
const { keyId, scope } = stored;
|
|
643
|
+
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);
|
|
644
|
+
else {
|
|
645
|
+
const client = getApiClient({
|
|
646
|
+
apiKey: stored.apiKey,
|
|
647
|
+
apiHost: props.apiHost
|
|
648
|
+
});
|
|
649
|
+
log.info(await withdrawKey(client, scope, keyId) ? `Revoked the API key (id ${keyId})` : `Could not revoke the API key (id ${keyId}) — it may still be live. Remove it with: neon api-keys revoke ${keyId}${scope.orgId ? ` --org-id ${scope.orgId}` : ""}`);
|
|
650
|
+
}
|
|
651
|
+
} else if (stored !== null) {
|
|
652
|
+
const revoked = await revokeTokenSet(stored.tokens, props);
|
|
653
|
+
log.info(revoked ? "Revoked the OAuth token" : "Could not revoke the OAuth token — removing locally anyway");
|
|
654
|
+
}
|
|
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);
|
|
669
|
+
}
|
|
670
|
+
} else if (name === "DEFAULT") log.info("Signed out of DEFAULT");
|
|
902
671
|
};
|
|
903
672
|
/** Revoke the OAuth refresh token in a credential we already have in hand. */
|
|
904
673
|
const revokeTokenSet = async (credentials, props) => {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
: {}),
|
|
912
|
-
}, tokenSet);
|
|
674
|
+
const tokenSet = credentials;
|
|
675
|
+
return await revokeToken({
|
|
676
|
+
oauthHost: props.oauthHost,
|
|
677
|
+
clientId: props.clientId,
|
|
678
|
+
...props.allowUnsafeTls ? { allowUnsafeTls: props.allowUnsafeTls } : {}
|
|
679
|
+
}, tokenSet);
|
|
913
680
|
};
|
|
681
|
+
//#endregion
|
|
682
|
+
export { aliases, builder, command, describe, handler, profile_exports as t };
|