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