neon 2.42.0 → 2.44.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 +181 -12
- package/dist/_shared/auth_selection.js +86 -0
- package/dist/_shared/credentials.js +209 -0
- package/dist/_shared/paths.js +149 -0
- package/dist/{profiles.js → _shared/profiles.js} +121 -35
- package/dist/_shared/secure_file.js +43 -0
- package/dist/analytics.js +16 -6
- package/dist/api.js +98 -10
- package/dist/auth_context.js +53 -8
- package/dist/commands/api_keys.js +349 -0
- package/dist/commands/auth.js +131 -59
- package/dist/commands/bootstrap.js +16 -3
- package/dist/commands/index.js +2 -0
- package/dist/commands/init.js +17 -0
- package/dist/commands/profile.js +831 -41
- package/dist/config.js +1 -22
- package/dist/context.js +19 -0
- package/dist/index.js +16 -9
- package/dist/profile_keys.js +55 -0
- package/dist/utils/flags.js +52 -0
- package/dist/utils/middlewares.js +16 -2
- package/dist/utils/package_manager.js +8 -1
- package/package.json +13 -13
package/dist/auth_context.js
CHANGED
|
@@ -1,16 +1,61 @@
|
|
|
1
|
+
import { isOwnedCredentialPath } from "./config.js";
|
|
1
2
|
let current = null;
|
|
2
3
|
export const setAuthContext = (context) => {
|
|
3
4
|
current = context;
|
|
4
5
|
};
|
|
5
6
|
export const getAuthContext = () => current;
|
|
7
|
+
/** Reset between tests, so one case cannot observe another's authentication. */
|
|
8
|
+
export const clearAuthContext = () => {
|
|
9
|
+
current = null;
|
|
10
|
+
};
|
|
6
11
|
/**
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
12
|
+
* The credentials file a 401 should delete, or `null` to leave everything on disk.
|
|
13
|
+
*
|
|
14
|
+
* Only an expired OAuth token set is worth clearing: deleting it makes the next command log
|
|
15
|
+
* in again, which is the recovery. Neither key-shaped source is.
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* A key passed on the command line was never ours to store, so a 401 on it says nothing about
|
|
18
|
+
* any stored credential — clearing one would sign the user out of an account the failed
|
|
19
|
+
* request never used.
|
|
20
|
+
*
|
|
21
|
+
* A key read from a profile must survive for a sharper reason: unlike an OAuth token there is
|
|
22
|
+
* nothing to refresh and no automatic way back, so deleting it would destroy the only copy of
|
|
23
|
+
* a credential the user has to paste or mint again. It is also the expected state during
|
|
24
|
+
* rotation, where the whole point is that the old key is dead and the file must still be there
|
|
25
|
+
* to be replaced.
|
|
15
26
|
*/
|
|
16
|
-
export const credentialsToClearOn401 = (context) =>
|
|
27
|
+
export const credentialsToClearOn401 = (context) => {
|
|
28
|
+
if (context?.source !== "stored-credentials")
|
|
29
|
+
return null;
|
|
30
|
+
const path = context.credentialsPath;
|
|
31
|
+
if (path === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
// Only a file the CLI created. A profile entry may point anywhere, and a credentials file
|
|
34
|
+
// we merely adopted is not ours to delete — `neon profile remove` already refuses to touch
|
|
35
|
+
// one, so a 401 must not quietly do what an explicit removal declines to.
|
|
36
|
+
//
|
|
37
|
+
// The legacy `neonctl` directory counts as ours: default resolution deliberately still
|
|
38
|
+
// reads it in place, so an install predating the rename would otherwise have its own
|
|
39
|
+
// credentials called "adopted" and never cleared.
|
|
40
|
+
return isOwnedCredentialPath(context.configDir, path) ? path : null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* What to tell the user when the API rejects their credential, naming the profile and file
|
|
44
|
+
* when one is involved so they know which of several accounts failed and what to re-run.
|
|
45
|
+
*/
|
|
46
|
+
export const authFailureMessage = (context) => {
|
|
47
|
+
const profile = context?.profile ?? "the selected profile";
|
|
48
|
+
const where = context?.credentialsPath !== undefined
|
|
49
|
+
? ` (${context.credentialsPath})`
|
|
50
|
+
: "";
|
|
51
|
+
if (context?.source === "profile-api-key") {
|
|
52
|
+
// Not `rotate-key`: a rejected key cannot authenticate to mint its own replacement.
|
|
53
|
+
return `Authentication failed: the Neon API rejected profile "${profile}"'s API key${where}. Replace it with \`neon profile create ${profile} --mint --force\`, or store another with \`neon profile create ${profile} --api-key - --force\`.`;
|
|
54
|
+
}
|
|
55
|
+
// Reached only when the session was not ours to clear, i.e. an adopted credentials file.
|
|
56
|
+
// Saying "check --api-key" there would be nonsense; the fix is to sign in again.
|
|
57
|
+
if (context?.source === "stored-credentials") {
|
|
58
|
+
return `Authentication failed: the Neon API rejected profile "${profile}"'s stored session${where}. That file was not created by neon, so it was left alone — sign in again with \`neon auth --profile ${profile}\`.`;
|
|
59
|
+
}
|
|
60
|
+
return "Authentication failed: the Neon API rejected the API key. Check --api-key or NEON_API_KEY.";
|
|
61
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { isNeonApiError } from "../api.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
|
+
import { noPassthrough, single } from "../utils/flags.js";
|
|
4
|
+
import { writer } from "../writer.js";
|
|
5
|
+
const ACCOUNT_FIELDS = [
|
|
6
|
+
"id",
|
|
7
|
+
"name",
|
|
8
|
+
"created_at",
|
|
9
|
+
"last_used_at",
|
|
10
|
+
"last_used_from_addr",
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Table view of an org listing. `project` is the rendered column; the raw `project_id` is
|
|
14
|
+
* what structured output keeps. See {@link list} for why the two differ.
|
|
15
|
+
*/
|
|
16
|
+
const ORG_TABLE_FIELDS = [
|
|
17
|
+
"id",
|
|
18
|
+
"name",
|
|
19
|
+
"project",
|
|
20
|
+
"created_at",
|
|
21
|
+
"last_used_at",
|
|
22
|
+
"last_used_from_addr",
|
|
23
|
+
];
|
|
24
|
+
const ORG_FIELDS = [
|
|
25
|
+
"id",
|
|
26
|
+
"name",
|
|
27
|
+
"project_id",
|
|
28
|
+
"created_at",
|
|
29
|
+
"last_used_at",
|
|
30
|
+
"last_used_from_addr",
|
|
31
|
+
];
|
|
32
|
+
const CREATE_FIELDS = ["id", "name", "key"];
|
|
33
|
+
/** Metadata only: the secret is printed separately so it can be copied cleanly. */
|
|
34
|
+
const CREATE_TABLE_FIELDS = ["id", "name"];
|
|
35
|
+
const CREATE_TABLE_FIELDS_SCOPED = ["id", "name", "project"];
|
|
36
|
+
const CREATE_FIELDS_SCOPED = ["id", "name", "project_id", "key"];
|
|
37
|
+
/** Rendered for an org key that was not narrowed to a project. Table output only. */
|
|
38
|
+
const ALL_PROJECTS = "(all projects)";
|
|
39
|
+
/**
|
|
40
|
+
* "This key should carry no project scope."
|
|
41
|
+
*
|
|
42
|
+
* A symbol rather than a string, because project ids are `^[a-z0-9-]{1,60}$` — any sentinel
|
|
43
|
+
* spelled as a string is a legal project id, and a project genuinely called that would have
|
|
44
|
+
* its correctly-scoped key rejected and revoked.
|
|
45
|
+
*/
|
|
46
|
+
const NO_PROJECT = Symbol("no-project");
|
|
47
|
+
export const command = "api-keys";
|
|
48
|
+
export const aliases = ["api-key"];
|
|
49
|
+
export const describe = "Manage API keys";
|
|
50
|
+
export const builder = (argv) => argv
|
|
51
|
+
.usage("$0 api-keys <sub-command> [options]")
|
|
52
|
+
.command("list", "List API keys for your account, or for an organization", (yargs) => yargs
|
|
53
|
+
.options({
|
|
54
|
+
"org-id": {
|
|
55
|
+
describe: "List the organization's keys instead of your account's",
|
|
56
|
+
type: "string",
|
|
57
|
+
coerce: single("org-id"),
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
.strict()
|
|
61
|
+
.check(noPassthrough("api-keys")), async (args) => await list(args))
|
|
62
|
+
.command("create", "Create an API key. The key is shown once and cannot be retrieved again", (yargs) => yargs
|
|
63
|
+
.options({
|
|
64
|
+
name: {
|
|
65
|
+
describe: "A name to identify the key later",
|
|
66
|
+
type: "string",
|
|
67
|
+
demandOption: true,
|
|
68
|
+
coerce: single("name", { required: true }),
|
|
69
|
+
},
|
|
70
|
+
"org-id": {
|
|
71
|
+
describe: "Create a key for this organization instead of your account",
|
|
72
|
+
type: "string",
|
|
73
|
+
coerce: single("org-id"),
|
|
74
|
+
},
|
|
75
|
+
"project-id": {
|
|
76
|
+
describe: "Create a key that can access only this project. Its organization is looked up from the project",
|
|
77
|
+
type: "string",
|
|
78
|
+
coerce: single("project-id"),
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
// A key scoped to a project is already an organization key, so naming
|
|
82
|
+
// both is contradictory rather than redundant — the org is derived from
|
|
83
|
+
// the project and cannot be chosen independently.
|
|
84
|
+
.conflicts("org-id", "project-id")
|
|
85
|
+
.strict()
|
|
86
|
+
.check(noPassthrough("api-keys")), async (args) => await create(args))
|
|
87
|
+
.command("revoke <id>", "Revoke an API key. Anything using it stops working immediately", (yargs) => yargs
|
|
88
|
+
.positional("id", {
|
|
89
|
+
describe: "The API key id, from `api-keys list`",
|
|
90
|
+
// Deliberately not `type: "number"`. That coerces before `coerce`
|
|
91
|
+
// runs, so an unparseable id arrives as NaN and the error can only
|
|
92
|
+
// echo `NaN` — a JavaScript artifact the user never typed. Taking
|
|
93
|
+
// the raw string lets the message name what they actually passed,
|
|
94
|
+
// and stops NaN reaching the API, which answers "not found" and
|
|
95
|
+
// blames the key rather than the input.
|
|
96
|
+
type: "string",
|
|
97
|
+
demandOption: true,
|
|
98
|
+
coerce: (value) => {
|
|
99
|
+
// Digits only, checked before Number(): `Number("0x65")` is 101
|
|
100
|
+
// and `Number("1e3")` is 1000, so either would revoke a key the
|
|
101
|
+
// user never named while the message promises "a numeric id".
|
|
102
|
+
const id = Number(value);
|
|
103
|
+
if (typeof value !== "string" ||
|
|
104
|
+
!/^\d+$/.test(value.trim()) ||
|
|
105
|
+
!Number.isSafeInteger(id) ||
|
|
106
|
+
id <= 0) {
|
|
107
|
+
throw new Error(`api-keys revoke needs a numeric key id, from \`neon api-keys list\`. Got \`${String(value)}\`.`);
|
|
108
|
+
}
|
|
109
|
+
return id;
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.options({
|
|
113
|
+
"org-id": {
|
|
114
|
+
describe: "Revoke an organization key instead of an account key",
|
|
115
|
+
type: "string",
|
|
116
|
+
coerce: single("org-id"),
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
.strict()
|
|
120
|
+
.check(noPassthrough("api-keys")), async (args) => await revoke(args))
|
|
121
|
+
.demandCommand(1, "Run `neon api-keys --help` to see the subcommands.");
|
|
122
|
+
export const handler = (args) => args;
|
|
123
|
+
const list = async (props) => {
|
|
124
|
+
const out = writer(props);
|
|
125
|
+
if (props.orgId) {
|
|
126
|
+
const { data } = await props.apiClient.listOrgApiKeys(props.orgId);
|
|
127
|
+
// `writeTable` drops any column empty in every row, so an all-absent `project_id`
|
|
128
|
+
// would take the whole column with it — hiding the answer exactly when it is "none
|
|
129
|
+
// of them are scoped". Fill it in for the table, but leave structured output alone:
|
|
130
|
+
// json/yaml serialize the whole object, so a synthetic field there would change the
|
|
131
|
+
// machine-readable shape and duplicate `project_id` under a second name.
|
|
132
|
+
if (props.output === "table") {
|
|
133
|
+
out.write(data.map((key) => ({
|
|
134
|
+
...key,
|
|
135
|
+
project: key.project_id ?? ALL_PROJECTS,
|
|
136
|
+
})), {
|
|
137
|
+
fields: ORG_TABLE_FIELDS,
|
|
138
|
+
title: `API keys in ${props.orgId}`,
|
|
139
|
+
emptyMessage: `No API keys in ${props.orgId}.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
out.write(data, {
|
|
144
|
+
fields: ORG_FIELDS,
|
|
145
|
+
title: `API keys in ${props.orgId}`,
|
|
146
|
+
emptyMessage: `No API keys in ${props.orgId}.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
out.end();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const { data } = await props.apiClient.listApiKeys();
|
|
153
|
+
out.write(data, {
|
|
154
|
+
fields: ACCOUNT_FIELDS,
|
|
155
|
+
title: "Account API keys",
|
|
156
|
+
emptyMessage: "You have no account API keys.",
|
|
157
|
+
});
|
|
158
|
+
out.end();
|
|
159
|
+
// Organization keys live on a different endpoint, so a heading of plain "API keys"
|
|
160
|
+
// would claim to be everything while showing only half.
|
|
161
|
+
if (props.output === "table") {
|
|
162
|
+
log.info("Organization keys are listed separately: neon api-keys list --org-id <org> (see `neon orgs list`).");
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const create = async (props) => {
|
|
166
|
+
const { name, projectId, orgId } = props;
|
|
167
|
+
// Neither flag: an account key, matching `POST /api_keys`. `api-keys` is exempt from
|
|
168
|
+
// `.neon` enrichment (see `isApiKeysCommand`), so reaching this branch means the user
|
|
169
|
+
// really did ask for account scope rather than inheriting a checked-out project.
|
|
170
|
+
if (!projectId && !orgId) {
|
|
171
|
+
const { data } = await props.apiClient.createApiKey({ key_name: name });
|
|
172
|
+
await assertUsable(props, data, { orgId: null, expect: NO_PROJECT });
|
|
173
|
+
report(props, data, CREATE_FIELDS, CREATE_TABLE_FIELDS);
|
|
174
|
+
// The only key here that reaches everything, and the only one that used to say
|
|
175
|
+
// nothing about its reach.
|
|
176
|
+
log.warning("This key reaches everything your account can, in every organization. Pass --org-id or --project-id to narrow it.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!projectId && orgId) {
|
|
180
|
+
const { data } = await props.apiClient.createOrgApiKey(orgId, {
|
|
181
|
+
key_name: name,
|
|
182
|
+
});
|
|
183
|
+
await assertUsable(props, data, { orgId, expect: NO_PROJECT });
|
|
184
|
+
report(props, data, CREATE_FIELDS, CREATE_TABLE_FIELDS);
|
|
185
|
+
log.warning("This key reaches every project in %s, including ones created later. Pass --project-id instead to restrict it to one.", orgId);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const scopeTo = projectId;
|
|
189
|
+
// Project-scoped keys exist only on the organization endpoint, so an org is required.
|
|
190
|
+
// Resolve it from the project rather than asking for both: `--project-id` alone would
|
|
191
|
+
// otherwise fail for a reason that isn't visible from the command line.
|
|
192
|
+
const resolvedOrgId = await orgIdForProject(props.apiClient, scopeTo);
|
|
193
|
+
const { data } = await props.apiClient.createOrgApiKey(resolvedOrgId, {
|
|
194
|
+
key_name: name,
|
|
195
|
+
project_id: scopeTo,
|
|
196
|
+
});
|
|
197
|
+
// `project_id` is optional on the response. Printing a key and calling it scoped
|
|
198
|
+
// without checking would hand over a credential whose reach we never confirmed — the
|
|
199
|
+
// one thing this command must not get wrong.
|
|
200
|
+
await assertUsable(props, data, {
|
|
201
|
+
orgId: resolvedOrgId,
|
|
202
|
+
expect: scopeTo,
|
|
203
|
+
});
|
|
204
|
+
report(props, data, CREATE_FIELDS_SCOPED, CREATE_TABLE_FIELDS_SCOPED);
|
|
205
|
+
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.", scopeTo);
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Print the issued key.
|
|
209
|
+
*
|
|
210
|
+
* In a terminal the secret goes on its own line rather than into a table cell: `cli-table`
|
|
211
|
+
* neither wraps nor truncates, so a 50-odd character key makes the row wider than most
|
|
212
|
+
* terminals and the wrapped remainder ends up beside box-drawing characters. Since this is
|
|
213
|
+
* the only time the key is ever shown, it has to be selectable in one gesture. Structured
|
|
214
|
+
* output keeps the key in the object, where a script expects it.
|
|
215
|
+
*/
|
|
216
|
+
const report = (props, data, fields, tableFields) => {
|
|
217
|
+
const out = writer(props);
|
|
218
|
+
if (props.output === "table") {
|
|
219
|
+
// `project` mirrors the column name `list` uses. Added here rather than by the
|
|
220
|
+
// caller so structured output keeps the API's own `project_id` and gains no
|
|
221
|
+
// duplicate under a second name.
|
|
222
|
+
const row = typeof data.project_id === "string"
|
|
223
|
+
? { ...data, project: data.project_id }
|
|
224
|
+
: data;
|
|
225
|
+
out.write(row, {
|
|
226
|
+
fields: tableFields,
|
|
227
|
+
title: "API key",
|
|
228
|
+
});
|
|
229
|
+
out.end();
|
|
230
|
+
// Blank line so the key is visually detached from the table border, and so
|
|
231
|
+
// `| tail -1` on stdout yields exactly the key (both notices go to stderr).
|
|
232
|
+
out.text(`\n${data.key}\n`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
out.write(data, { fields: fields, title: "API key" });
|
|
236
|
+
out.end();
|
|
237
|
+
}
|
|
238
|
+
log.warning("Store this key now: it is not shown again.");
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Refuse to report a key that isn't what we asked for, and take it back.
|
|
242
|
+
*
|
|
243
|
+
* Two ways a 2xx can still be wrong: no `key` in the body, which leaves a live credential
|
|
244
|
+
* the user can never see or use; and a `project_id` that doesn't match the requested
|
|
245
|
+
* project, which would mean announcing a scope the key does not have. Both withdraw the key
|
|
246
|
+
* before throwing.
|
|
247
|
+
*
|
|
248
|
+
* The thrown message states whether the withdrawal actually succeeded. Saying "the key has
|
|
249
|
+
* been revoked" when the revoke itself failed would be worse than saying nothing — it would
|
|
250
|
+
* leave an unverified credential live while telling the user it is gone.
|
|
251
|
+
*/
|
|
252
|
+
const assertUsable = async (props, data, scope) => {
|
|
253
|
+
// `expect` is always stated, never inferred from a missing field: "no project" has to be
|
|
254
|
+
// checked as deliberately as an exact project, or a key that came back narrower than
|
|
255
|
+
// requested would be reported as reaching the whole organization.
|
|
256
|
+
const wanted = scope.expect === NO_PROJECT ? undefined : scope.expect;
|
|
257
|
+
const problem = typeof data.key !== "string" || data.key.trim() === ""
|
|
258
|
+
? "Neon returned no key."
|
|
259
|
+
: data.project_id !== wanted
|
|
260
|
+
? `Neon returned a key scoped to ${data.project_id ?? "nothing"} rather than ${wanted ?? "the whole organization"}.`
|
|
261
|
+
: null;
|
|
262
|
+
if (!problem)
|
|
263
|
+
return;
|
|
264
|
+
const withdrawn = await withdraw(props, scope.orgId, data.id);
|
|
265
|
+
throw new Error(`${problem} ${withdrawn
|
|
266
|
+
? "The key has been revoked; nothing was issued."
|
|
267
|
+
: `The key could NOT be revoked and may still be live${data.id === undefined
|
|
268
|
+
? ""
|
|
269
|
+
: `. Remove it with \`neon api-keys revoke ${data.id}${scope.orgId
|
|
270
|
+
? ` --org-id ${scope.orgId}`
|
|
271
|
+
: ""}\``}.`}`);
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Revoke, turning the most likely mistake into a usable message.
|
|
275
|
+
*
|
|
276
|
+
* Account and organization keys live on different endpoints, and `api-keys list --org-id X`
|
|
277
|
+
* shows ids that the account endpoint cannot see. Copying one and forgetting the flag — or
|
|
278
|
+
* leaving it on for an account key — is the easy error, and a bare "API key not found" sends
|
|
279
|
+
* the user looking for a deleted key rather than a misplaced flag.
|
|
280
|
+
*
|
|
281
|
+
* Only the key id is worth second-guessing here: an org id that is wrong or not yours does
|
|
282
|
+
* not reach this path at all, because the API answers "not an organization member" (verified
|
|
283
|
+
* against production) rather than a 404.
|
|
284
|
+
*/
|
|
285
|
+
const revokeOrExplain = async (props) => {
|
|
286
|
+
try {
|
|
287
|
+
return props.orgId
|
|
288
|
+
? await props.apiClient.revokeOrgApiKey(props.orgId, props.id)
|
|
289
|
+
: await props.apiClient.revokeApiKey(props.id);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
293
|
+
throw new Error(props.orgId
|
|
294
|
+
? `No API key with id ${props.id} in ${props.orgId}. If it is one of your account's own keys, drop --org-id.`
|
|
295
|
+
: `No account API key with id ${props.id}. If it belongs to an organization, pass --org-id. Organization keys are not visible to your account.`);
|
|
296
|
+
}
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
/** Best-effort withdrawal of a key we are refusing to report. Never throws. */
|
|
301
|
+
const withdraw = async (props, orgId, keyId) => {
|
|
302
|
+
if (!Number.isSafeInteger(keyId) || keyId <= 0)
|
|
303
|
+
return false;
|
|
304
|
+
try {
|
|
305
|
+
const { data } = orgId
|
|
306
|
+
? await props.apiClient.revokeOrgApiKey(orgId, keyId)
|
|
307
|
+
: await props.apiClient.revokeApiKey(keyId);
|
|
308
|
+
// Check which key the response names: a `revoked: true` for some other id is not
|
|
309
|
+
// evidence that the one we issued is gone.
|
|
310
|
+
return data.revoked === true && data.id === keyId;
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
log.error("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const revoke = async (props) => {
|
|
318
|
+
const out = writer(props);
|
|
319
|
+
const { data } = await revokeOrExplain(props);
|
|
320
|
+
out.write(data, {
|
|
321
|
+
fields: ["id", "name", "revoked", "last_used_at"],
|
|
322
|
+
title: "API key",
|
|
323
|
+
});
|
|
324
|
+
out.end();
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* The organization a project belongs to. A project outside any organization cannot have a
|
|
328
|
+
* scoped key — the endpoint that accepts `project_id` is org-only — so that case fails here
|
|
329
|
+
* with the reason, rather than as a 404 from a URL the user never typed.
|
|
330
|
+
*/
|
|
331
|
+
export const orgIdForProject = async (client, projectId) => {
|
|
332
|
+
let orgId;
|
|
333
|
+
try {
|
|
334
|
+
const { data: { project }, } = await client.getProject(projectId);
|
|
335
|
+
orgId = project.org_id;
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
339
|
+
throw new Error(projectId.startsWith("org-")
|
|
340
|
+
? `Project ${projectId} not found. That looks like an organization id. Pass it as --org-id instead.`
|
|
341
|
+
: `Project ${projectId} not found. Check the id with \`neon projects list\`.`);
|
|
342
|
+
}
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
if (!orgId) {
|
|
346
|
+
throw new Error(`Project ${projectId} does not belong to an organization, so it cannot have a project-scoped API key. Omit --project-id to create an account key.`);
|
|
347
|
+
}
|
|
348
|
+
return orgId;
|
|
349
|
+
};
|