neon 8.11.1 → 8.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +1 -1
  2. package/dist/_chunks/api-GbEaBZJ5.js +388 -0
  3. package/dist/_chunks/api_keys-CNnnugbQ.js +281 -0
  4. package/dist/_chunks/ask-DVwy0eUL.js +155 -0
  5. package/dist/_chunks/auth-CIUtXByB.js +495 -0
  6. package/dist/_chunks/bootstrap-wPZWa1sT.js +499 -0
  7. package/dist/_chunks/branches-BNa1xX1L.js +460 -0
  8. package/dist/_chunks/bucket-BCdryTkD.js +409 -0
  9. package/dist/_chunks/checkout-CXIOydqZ.js +348 -0
  10. package/dist/_chunks/claim-DqAvAvDm.js +473 -0
  11. package/dist/_chunks/config-BH_qRHiG.js +1113 -0
  12. package/dist/_chunks/connection_string-DB4awPzZ.js +154 -0
  13. package/dist/_chunks/credentials-BSjE9fRR.js +146 -0
  14. package/dist/_chunks/data_api-Djxj5ZQD.js +263 -0
  15. package/dist/_chunks/databases-Dok4wru7.js +84 -0
  16. package/dist/_chunks/deploy-D7B0Xfjg.js +33 -0
  17. package/dist/_chunks/dev-CSvxAecE.js +945 -0
  18. package/dist/_chunks/diff-DhxxbdQE.js +178 -0
  19. package/dist/_chunks/docs-CrVybkTP.js +197 -0
  20. package/dist/_chunks/env-5HS5qQGd.js +284 -0
  21. package/dist/_chunks/feedback-CMayyTa8.js +88 -0
  22. package/dist/_chunks/functions-Q0zuvtnk.js +464 -0
  23. package/dist/_chunks/git-H32TNt6M.js +356 -0
  24. package/dist/_chunks/init-BkA6RilG.js +111 -0
  25. package/dist/_chunks/inspect-g0wekylj.js +90 -0
  26. package/dist/_chunks/ip_allow-5lLFvIuE.js +106 -0
  27. package/dist/_chunks/link-BgtkOBws.js +936 -0
  28. package/dist/_chunks/logs-DWkpjzIH.js +291 -0
  29. package/dist/_chunks/mcp-Bo-7TKc7.js +253 -0
  30. package/dist/_chunks/neon_auth-CBs1_3qu.js +822 -0
  31. package/dist/_chunks/open-CX3BT3Tp.js +33 -0
  32. package/dist/_chunks/operations-D4I-AxVs.js +37 -0
  33. package/dist/_chunks/orgs-xGfANwHQ.js +52 -0
  34. package/dist/_chunks/plugins-DqD7BDO1.js +169 -0
  35. package/dist/_chunks/profile-yXOiKk9R.js +672 -0
  36. package/dist/_chunks/projects-BwI2Uo0z.js +439 -0
  37. package/dist/_chunks/psql-BM-doe8e.js +64 -0
  38. package/dist/_chunks/realtime-BRqIyGR-.js +79 -0
  39. package/dist/_chunks/roles-B4w3YT_7.js +79 -0
  40. package/dist/_chunks/set_context-DbrwoImy.js +38 -0
  41. package/dist/_chunks/skills-DyiBXKS3.js +255 -0
  42. package/dist/_chunks/snapshots-RJBzW8GM.js +338 -0
  43. package/dist/_chunks/status-DV8Z2kw3.js +44 -0
  44. package/dist/_chunks/triggers-BrRo1z4b.js +302 -0
  45. package/dist/_chunks/user-dlOGXbX9.js +60 -0
  46. package/dist/_chunks/vpc_endpoints-CNb0YfMo.js +105 -0
  47. package/dist/auth_recovery.js +1 -1
  48. package/dist/commands/api.js +1 -388
  49. package/dist/commands/api_keys.js +1 -281
  50. package/dist/commands/ask.js +1 -155
  51. package/dist/commands/auth.js +1 -495
  52. package/dist/commands/bootstrap.js +1 -499
  53. package/dist/commands/branches.js +1 -460
  54. package/dist/commands/bucket.js +1 -409
  55. package/dist/commands/checkout.js +1 -348
  56. package/dist/commands/claim.js +1 -473
  57. package/dist/commands/config.js +1 -1113
  58. package/dist/commands/connection_string.js +1 -154
  59. package/dist/commands/credentials.js +1 -146
  60. package/dist/commands/data_api.js +1 -263
  61. package/dist/commands/databases.js +1 -84
  62. package/dist/commands/deploy.js +1 -33
  63. package/dist/commands/dev.js +1 -945
  64. package/dist/commands/diff.js +1 -178
  65. package/dist/commands/docs.js +1 -197
  66. package/dist/commands/env.js +1 -284
  67. package/dist/commands/feedback.js +1 -88
  68. package/dist/commands/functions.js +1 -464
  69. package/dist/commands/functions_new.js +1 -1
  70. package/dist/commands/git.js +1 -356
  71. package/dist/commands/index.js +45 -45
  72. package/dist/commands/init.js +1 -111
  73. package/dist/commands/inspect.js +1 -90
  74. package/dist/commands/ip_allow.js +1 -106
  75. package/dist/commands/link.js +1 -936
  76. package/dist/commands/logs.js +1 -291
  77. package/dist/commands/mcp.js +1 -253
  78. package/dist/commands/neon_auth.js +1 -822
  79. package/dist/commands/open.js +1 -33
  80. package/dist/commands/operations.js +1 -37
  81. package/dist/commands/orgs.js +1 -52
  82. package/dist/commands/plugins.js +1 -169
  83. package/dist/commands/profile.js +1 -672
  84. package/dist/commands/projects.js +1 -439
  85. package/dist/commands/psql.js +1 -64
  86. package/dist/commands/realtime.js +1 -79
  87. package/dist/commands/roles.js +1 -79
  88. package/dist/commands/set_context.js +1 -38
  89. package/dist/commands/skills.js +1 -255
  90. package/dist/commands/snapshots.js +1 -338
  91. package/dist/commands/status.js +1 -44
  92. package/dist/commands/subscribe.js +1 -1
  93. package/dist/commands/triggers.js +1 -302
  94. package/dist/commands/user.js +1 -60
  95. package/dist/commands/vpc_endpoints.js +1 -105
  96. package/dist/functions/registry.js +29 -151
  97. package/dist/functions/source.js +42 -2
  98. package/dist/index.js +1 -1
  99. package/dist/init/auth.js +3 -3
  100. package/dist/init/link.js +2 -2
  101. package/dist/init/run.js +3 -3
  102. package/dist/init/tooling.js +3 -3
  103. package/dist/mcp/mint.js +1 -1
  104. package/dist/test_utils/registry.js +1 -2
  105. package/package.json +8 -6
package/README.md CHANGED
@@ -1111,7 +1111,7 @@ Variables a template needs are declared on each function in `neon.ts` (`env: { A
1111
1111
 
1112
1112
  Nothing is written until every check passes: a file that already exists with different contents needs `--force`, and a slug `neon.ts` already declares needs `--slug`, `--force`, or `--no-add-to-config`. A `separate` template sets its own slugs, so `--slug` only changes its directory; remove the existing declarations from `neon.ts`, or use `--force` or `--no-add-to-config`.
1113
1113
 
1114
- The registry lives in [`registry/`](https://github.com/neondatabase/examples/tree/main/registry) in `neondatabase/examples`. The CLI reads the index from `https://neon.com/functions/registry/registry.json`, which serves `main` through a CDN cache, and falls back to raw GitHub when neon.com is unreachable. Template files are always read from raw GitHub. Set `NEON_FUNCTION_REGISTRY_DIR` to a local checkout to try templates before they are merged, or `NEON_FUNCTION_REGISTRY_URL` to read an index from somewhere else.
1114
+ The registry lives in [`registry/`](https://github.com/neondatabase/examples/tree/main/registry) in `neondatabase/examples`. The CLI reads the index from `https://neon.com/functions/registry/registry.json`, which serves `main` through a CDN cache, and falls back to raw GitHub when neon.com is unreachable. Template files are always read from raw GitHub. Index and template JSON are validated against the schemas at `https://neon.com/functions/registry/schemas/` (same GitHub fallback). Set `NEON_FUNCTION_REGISTRY_DIR` to a local checkout to try templates before they are merged, or `NEON_FUNCTION_REGISTRY_URL` to read an index from somewhere else.
1115
1115
 
1116
1116
  ## Function triggers (`triggers`)
1117
1117
 
@@ -0,0 +1,388 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { codeFromBody, isNeonApiError } from "../api.js";
3
+ import { writer } from "../writer.js";
4
+ import { describeOperation, getEndpoints, loadSpec } from "../utils/openapi.js";
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import chalk from "chalk";
8
+ import YAML from "yaml";
9
+ //#region src/commands/api.ts
10
+ var api_exports = /* @__PURE__ */ __exportAll({
11
+ buildBody: () => buildBody,
12
+ buildQuery: () => buildQuery,
13
+ builder: () => builder,
14
+ colorizeJson: () => colorizeJson,
15
+ command: () => command,
16
+ describe: () => describe,
17
+ handler: () => handler,
18
+ parseHeaders: () => parseHeaders,
19
+ parseKeyValue: () => parseKeyValue,
20
+ parseTypedValue: () => parseTypedValue,
21
+ setDeep: () => setDeep
22
+ });
23
+ const HTTP_METHODS = [
24
+ "GET",
25
+ "POST",
26
+ "PUT",
27
+ "PATCH",
28
+ "DELETE"
29
+ ];
30
+ /**
31
+ * Coerce a raw string into a typed JSON value: booleans, `null`, integers,
32
+ * floats, and JSON arrays/objects are parsed; everything else stays a string.
33
+ */
34
+ function parseTypedValue(raw) {
35
+ if (raw === "true") return true;
36
+ if (raw === "false") return false;
37
+ if (raw === "null") return null;
38
+ if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
39
+ if (/^-?\d*\.\d+$/.test(raw)) return Number.parseFloat(raw);
40
+ if (raw.startsWith("[") || raw.startsWith("{")) try {
41
+ return JSON.parse(raw);
42
+ } catch {
43
+ return raw;
44
+ }
45
+ return raw;
46
+ }
47
+ /** Split a `key=value` pair on the first `=`. */
48
+ function parseKeyValue(input) {
49
+ const eq = input.indexOf("=");
50
+ if (eq === -1) throw new Error(`Invalid "key=value" pair: "${input}".`);
51
+ return {
52
+ key: input.slice(0, eq),
53
+ value: input.slice(eq + 1)
54
+ };
55
+ }
56
+ /**
57
+ * Assign `value` into `target` at a dot-delimited path, creating intermediate
58
+ * objects as needed. Enables `-F branch.name=dev` → `{ branch: { name: "dev" } }`,
59
+ * matching the nested shape of Neon request bodies.
60
+ */
61
+ function setDeep(target, dottedKey, value) {
62
+ const parts = dottedKey.split(".");
63
+ let node = target;
64
+ for (let i = 0; i < parts.length - 1; i++) {
65
+ const part = parts[i];
66
+ const next = node[part];
67
+ if (typeof next !== "object" || next === null || Array.isArray(next)) node[part] = {};
68
+ node = node[part];
69
+ }
70
+ node[parts[parts.length - 1]] = value;
71
+ }
72
+ /** Build a JSON body object from typed `-F` and string `-f` field pairs. */
73
+ function buildBody(fields, rawFields) {
74
+ const body = {};
75
+ for (const field of fields) {
76
+ const { key, value } = parseKeyValue(field);
77
+ setDeep(body, key, parseTypedValue(value));
78
+ }
79
+ for (const field of rawFields) {
80
+ const { key, value } = parseKeyValue(field);
81
+ setDeep(body, key, value);
82
+ }
83
+ return body;
84
+ }
85
+ /** Build a query-string map from `-Q key=value` pairs. */
86
+ function buildQuery(pairs) {
87
+ const query = {};
88
+ for (const pair of pairs) {
89
+ const { key, value } = parseKeyValue(pair);
90
+ query[key] = value;
91
+ }
92
+ return query;
93
+ }
94
+ /** Build a header map from `-H key:value` pairs. */
95
+ function parseHeaders(pairs) {
96
+ const headers = {};
97
+ for (const pair of pairs) {
98
+ const idx = pair.indexOf(":");
99
+ if (idx === -1) throw new Error(`Invalid header "${pair}". Expected "key:value".`);
100
+ headers[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
101
+ }
102
+ return headers;
103
+ }
104
+ function assertMethod(method) {
105
+ if (!HTTP_METHODS.includes(method)) throw new Error(`Unsupported method "${method}". Use one of: ${HTTP_METHODS.join(", ")}.`);
106
+ }
107
+ const JSON_TOKEN = /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
108
+ /** Color the tokens of serialized JSON without changing a byte of the text itself. */
109
+ function colorizeJson(json) {
110
+ return json.replace(JSON_TOKEN, (token, string, colon, bool) => {
111
+ if (string !== void 0) return colon !== void 0 ? `${chalk.cyan(string)}${colon}` : chalk.green(string);
112
+ if (bool !== void 0) return chalk.magenta(token);
113
+ if (token === "null") return chalk.dim(token);
114
+ return chalk.yellow(token);
115
+ });
116
+ }
117
+ const METHOD_COLORS = {
118
+ GET: chalk.green,
119
+ POST: chalk.cyan,
120
+ PUT: chalk.yellow,
121
+ PATCH: chalk.yellow,
122
+ DELETE: chalk.red
123
+ };
124
+ async function readStdin() {
125
+ const chunks = [];
126
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
127
+ return Buffer.concat(chunks).toString("utf8");
128
+ }
129
+ /** Resolve `--data`: `-` reads stdin, `@file` reads a file, else the literal string. Parsed as JSON when possible. */
130
+ async function readData(data) {
131
+ let raw;
132
+ if (data === "-") raw = await readStdin();
133
+ else if (data.startsWith("@")) raw = readFileSync(resolve(data.slice(1)), "utf8");
134
+ else raw = data;
135
+ try {
136
+ return JSON.parse(raw);
137
+ } catch {
138
+ return raw;
139
+ }
140
+ }
141
+ const toStrings = (values) => (values ?? []).map(String);
142
+ async function listEndpoints(args) {
143
+ const spec = await loadSpec({
144
+ configDir: args.configDir,
145
+ specUrl: args.specUrl,
146
+ refresh: args.refresh
147
+ });
148
+ if (!spec) throw new Error(`Could not load the Neon OpenAPI spec from ${args.specUrl}. Check your network connection or pass --spec-url.`);
149
+ const endpoints = getEndpoints(spec).map((endpoint) => ({
150
+ method: endpoint.method,
151
+ path: endpoint.path,
152
+ summary: endpoint.summary ?? ""
153
+ }));
154
+ writer(args).end(endpoints, {
155
+ fields: [
156
+ "method",
157
+ "path",
158
+ "summary"
159
+ ],
160
+ title: `Neon API endpoints (${endpoints.length})`,
161
+ emptyMessage: "No endpoints found in the spec.",
162
+ renderColumns: { method: (endpoint) => useColor(args) ? (METHOD_COLORS[endpoint.method] ?? String)(endpoint.method) : endpoint.method }
163
+ });
164
+ }
165
+ function isListing(args) {
166
+ return args.list || args.path === "list" || args.path === "ls";
167
+ }
168
+ function describeType(field) {
169
+ const enumText = (values) => Array.isArray(values) && values.length > 0 ? ` (${values.map(String).join(", ")})` : "";
170
+ if (field.type === "array" && field.items) {
171
+ const props = field.items.properties;
172
+ if (props && props.length > 0) return `array (${props.map((prop) => `${prop.name}${enumText(prop.enum)}`).join(", ")})`;
173
+ return `array of ${field.items.type}${enumText(field.items.enum)}`;
174
+ }
175
+ return `${field.type}${enumText(field.enum)}`;
176
+ }
177
+ async function describeRoute(args) {
178
+ const path = args.path;
179
+ if (!path) throw new Error("Missing API path. Usage: neon api <path> --describe (e.g. neon api /projects --describe). Run `neon api --list` to see available routes.");
180
+ if (!path.startsWith("/")) throw new Error(`Invalid path "${path}". API paths must start with "/". Run \`neon api --list\` to see available routes.`);
181
+ if ([
182
+ ...toStrings(args.field),
183
+ ...toStrings(args.rawField),
184
+ ...toStrings(args.query),
185
+ ...toStrings(args.header)
186
+ ].length > 0 || args.data !== void 0 || args.include) throw new Error("--describe prints the field list; it does not send a request. Drop -F, -f, -d, -Q, -H, and -i.");
187
+ const spec = await loadSpec({
188
+ configDir: args.configDir,
189
+ specUrl: args.specUrl,
190
+ refresh: args.refresh
191
+ });
192
+ if (!spec) throw new Error(`Could not load the Neon OpenAPI spec from ${args.specUrl}. Check your network connection or pass --spec-url.`);
193
+ const method = String(args.method ?? "GET").toUpperCase();
194
+ assertMethod(method);
195
+ const description = describeOperation(spec, path, method);
196
+ const endpoint = {
197
+ method: description.method,
198
+ path: description.path,
199
+ summary: description.summary,
200
+ operationId: description.operationId,
201
+ bodyRequired: description.bodyRequired,
202
+ ...description.contentType !== "" && description.contentType !== "application/json" ? { contentType: description.contentType } : {}
203
+ };
204
+ let title = description.summary ? `${description.method} ${description.path} - ${description.summary}` : `${description.method} ${description.path}`;
205
+ if (description.bodyRequired) title = `${title} (body required)`;
206
+ if (description.contentType !== "" && description.contentType !== "application/json") title = `${title} (${description.contentType})`;
207
+ if (args.output === "json" || args.output === "yaml") {
208
+ writer(args).write(endpoint, {
209
+ fields: [
210
+ "method",
211
+ "path",
212
+ "summary",
213
+ "operationId",
214
+ "bodyRequired"
215
+ ],
216
+ title: "Endpoint"
217
+ }).end(description.fields, {
218
+ fields: [
219
+ "in",
220
+ "name",
221
+ "required",
222
+ "type",
223
+ "description"
224
+ ],
225
+ title: "Parameters"
226
+ });
227
+ return;
228
+ }
229
+ writer(args).end(description.fields, {
230
+ fields: [
231
+ "in",
232
+ "name",
233
+ "required",
234
+ "type",
235
+ "description"
236
+ ],
237
+ title,
238
+ emptyMessage: "No path, query, or body fields in the spec.",
239
+ renderColumns: {
240
+ required: (field) => field.required ? "required" : "optional",
241
+ type: (field) => describeType(field)
242
+ }
243
+ });
244
+ }
245
+ async function runRequest(args) {
246
+ const path = args.path;
247
+ if (!path) throw new Error("Missing API path. Usage: neon api <path> (e.g. neon api /projects). Run `neon api --list` to see available routes.");
248
+ if (!path.startsWith("/")) throw new Error(`Invalid path "${path}". API paths must start with "/". Run \`neon api --list\` to see available routes.`);
249
+ const fields = toStrings(args.field);
250
+ const rawFields = toStrings(args.rawField);
251
+ let body;
252
+ if (args.data !== void 0) body = await readData(args.data);
253
+ else if (fields.length > 0 || rawFields.length > 0) body = buildBody(fields, rawFields);
254
+ const hasBody = body !== void 0;
255
+ const method = String(args.method ?? (hasBody ? "POST" : "GET")).toUpperCase();
256
+ assertMethod(method);
257
+ const query = buildQuery(toStrings(args.query));
258
+ const headers = parseHeaders(toStrings(args.header));
259
+ const params = {
260
+ path,
261
+ method,
262
+ ...Object.keys(query).length > 0 ? { query } : {},
263
+ ...hasBody ? { body } : {},
264
+ ...Object.keys(headers).length > 0 ? { headers } : {}
265
+ };
266
+ let response;
267
+ try {
268
+ response = await args.apiClient.request(params);
269
+ } catch (err) {
270
+ if (args.include && isNeonApiError(err) && err.status !== void 0 && err.status !== 401 && !isSsoCode(codeFromBody(err.data))) await writeResponse(args, {
271
+ status: err.status,
272
+ statusText: err.statusText ?? "",
273
+ headers: err.headers ?? {},
274
+ data: err.data
275
+ });
276
+ throw err;
277
+ }
278
+ await writeResponse(args, response);
279
+ }
280
+ const isSsoCode = (code) => code === "SSO_AUTHORIZATION_REQUIRED" || code === "SSO_ORG_CREDS_ONLY";
281
+ /** Pipes stay plain regardless of FORCE_COLOR, so `neon api … | jq` never sees escapes. */
282
+ const useColor = (args) => process.stdout.isTTY === true && args.color !== false && !process.env.NO_COLOR && chalk.level > 0;
283
+ /** Resolves once stdout has taken the text, so a following `process.exit` can't cut it off. */
284
+ async function writeResponse(args, response) {
285
+ const color = useColor(args);
286
+ let text = "";
287
+ if (args.include) {
288
+ const status = `HTTP ${response.status} ${response.statusText}`;
289
+ const statusColor = response.status < 400 ? chalk.green : chalk.red;
290
+ text += `${color ? statusColor.bold(status) : status}\n`;
291
+ for (const [key, value] of Object.entries(response.headers)) text += `${color ? chalk.cyan(key) : key}: ${value}\n`;
292
+ text += "\n";
293
+ }
294
+ if (response.data !== void 0) {
295
+ if (args.output === "yaml") text += YAML.stringify(response.data);
296
+ else {
297
+ const json = JSON.stringify(response.data, null, 2);
298
+ text += `${color ? colorizeJson(json) : json}\n`;
299
+ }
300
+ }
301
+ if (text === "") return;
302
+ await new Promise((resolve, reject) => {
303
+ process.stdout.write(text, (err) => err ? reject(err) : resolve());
304
+ });
305
+ }
306
+ const command = "api [path]";
307
+ const describe = "Call any Neon API route directly (authenticated passthrough)";
308
+ const builder = (argv) => argv.usage("$0 api <path> [options]").positional("path", {
309
+ type: "string",
310
+ describe: "API path beginning with \"/\" (e.g. /projects or /projects/{project_id}/branches). Use `list` to list endpoints."
311
+ }).options({
312
+ method: {
313
+ alias: "X",
314
+ type: "string",
315
+ describe: "HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to GET, or POST when a body is provided."
316
+ },
317
+ field: {
318
+ alias: "F",
319
+ type: "array",
320
+ string: true,
321
+ describe: "Body field key=value (repeatable). Dot-notation nests objects (e.g. -F branch.name=dev); values are typed (numbers, booleans, null, JSON)."
322
+ },
323
+ "raw-field": {
324
+ alias: "f",
325
+ type: "array",
326
+ string: true,
327
+ describe: "Body field key=value with the value kept as a raw string."
328
+ },
329
+ data: {
330
+ alias: "d",
331
+ type: "string",
332
+ describe: "Raw request body: a JSON string, @file, or - for stdin. Overrides --field."
333
+ },
334
+ query: {
335
+ alias: "Q",
336
+ type: "array",
337
+ string: true,
338
+ describe: "Query parameter key=value (repeatable)."
339
+ },
340
+ header: {
341
+ alias: "H",
342
+ type: "array",
343
+ string: true,
344
+ describe: "Extra request header key:value (repeatable)."
345
+ },
346
+ include: {
347
+ alias: "i",
348
+ type: "boolean",
349
+ default: false,
350
+ describe: "Print the response status and headers before the body."
351
+ },
352
+ list: {
353
+ type: "boolean",
354
+ default: false,
355
+ describe: "List available API endpoints from the OpenAPI spec."
356
+ },
357
+ describe: {
358
+ type: "boolean",
359
+ default: false,
360
+ describe: "Print path, query, and body fields from the OpenAPI spec without calling the API. Body names are dotted for -F."
361
+ },
362
+ refresh: {
363
+ type: "boolean",
364
+ default: false,
365
+ describe: "Refresh the cached OpenAPI spec (used with --list and --describe)."
366
+ },
367
+ "spec-url": {
368
+ type: "string",
369
+ default: process.env.NEON_API_SPEC_URL ?? "https://neon.com/api_spec/release/v2.json",
370
+ hidden: true,
371
+ describe: "OpenAPI spec URL used by --list and --describe."
372
+ }
373
+ }).example("$0 api /projects", "List your projects").example("$0 api /projects/{id}/branches -X POST -F branch.name=dev", "Create a branch").example("$0 api --list", "List every available API route").example("$0 api /projects --describe", "Show GET /projects query parameters").example("$0 api /projects -X POST --describe", "Show the create-project body fields");
374
+ const handler = async (args) => {
375
+ const apiArgs = args;
376
+ if (isListing(apiArgs) && apiArgs.describe) throw new Error("Pass either --list or --describe, not both.");
377
+ if (isListing(apiArgs)) {
378
+ await listEndpoints(apiArgs);
379
+ return;
380
+ }
381
+ if (apiArgs.describe) {
382
+ await describeRoute(apiArgs);
383
+ return;
384
+ }
385
+ await runRequest(apiArgs);
386
+ };
387
+ //#endregion
388
+ export { colorizeJson as a, handler as c, parseTypedValue as d, setDeep as f, builder as i, parseHeaders as l, buildBody as n, command as o, buildQuery as r, describe as s, api_exports as t, parseKeyValue as u };
@@ -0,0 +1,281 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { log } from "../log.js";
3
+ import { isNeonApiError } from "../api.js";
4
+ import { writer } from "../writer.js";
5
+ import { noPassthrough, single } from "../utils/flags.js";
6
+ //#region src/commands/api_keys.ts
7
+ var api_keys_exports = /* @__PURE__ */ __exportAll({
8
+ aliases: () => aliases,
9
+ builder: () => builder,
10
+ command: () => command,
11
+ describe: () => describe,
12
+ handler: () => handler,
13
+ orgIdForProject: () => orgIdForProject
14
+ });
15
+ const ACCOUNT_FIELDS = [
16
+ "id",
17
+ "name",
18
+ "created_at",
19
+ "last_used_at",
20
+ "last_used_from_addr"
21
+ ];
22
+ /**
23
+ * Table view of an org listing. `project` is the rendered column; the raw `project_id` is
24
+ * what structured output keeps. See {@link list} for why the two differ.
25
+ */
26
+ const ORG_TABLE_FIELDS = [
27
+ "id",
28
+ "name",
29
+ "project",
30
+ "created_at",
31
+ "last_used_at",
32
+ "last_used_from_addr"
33
+ ];
34
+ const ORG_FIELDS = [
35
+ "id",
36
+ "name",
37
+ "project_id",
38
+ "created_at",
39
+ "last_used_at",
40
+ "last_used_from_addr"
41
+ ];
42
+ const CREATE_FIELDS = [
43
+ "id",
44
+ "name",
45
+ "key"
46
+ ];
47
+ /** Metadata only: the secret is printed separately so it can be copied cleanly. */
48
+ const CREATE_TABLE_FIELDS = ["id", "name"];
49
+ const CREATE_TABLE_FIELDS_SCOPED = [
50
+ "id",
51
+ "name",
52
+ "project"
53
+ ];
54
+ const CREATE_FIELDS_SCOPED = [
55
+ "id",
56
+ "name",
57
+ "project_id",
58
+ "key"
59
+ ];
60
+ /** Rendered for an org key that was not narrowed to a project. Table output only. */
61
+ const ALL_PROJECTS = "(all projects)";
62
+ /**
63
+ * "This key should carry no project scope."
64
+ *
65
+ * A symbol rather than a string, because project ids are `^[a-z0-9-]{1,60}$` — any sentinel
66
+ * spelled as a string is a legal project id, and a project genuinely called that would have
67
+ * its correctly-scoped key rejected and revoked.
68
+ */
69
+ const NO_PROJECT = Symbol("no-project");
70
+ const command = "api-keys";
71
+ const aliases = ["api-key"];
72
+ const describe = "Manage API keys";
73
+ const builder = (argv) => argv.usage("$0 api-keys <sub-command> [options]").command("list", "List API keys for your account, or for an organization", (yargs) => yargs.options({ "org-id": {
74
+ describe: "List the organization's keys instead of your account's",
75
+ type: "string",
76
+ coerce: single("org-id")
77
+ } }).strict().check(noPassthrough("api-keys")), async (args) => await list(args)).command("create", "Create an API key. The key is shown once and cannot be retrieved again", (yargs) => yargs.options({
78
+ name: {
79
+ describe: "A name to identify the key later",
80
+ type: "string",
81
+ demandOption: true,
82
+ coerce: single("name", { required: true })
83
+ },
84
+ "org-id": {
85
+ describe: "Create a key for this organization instead of your account",
86
+ type: "string",
87
+ coerce: single("org-id")
88
+ },
89
+ "project-id": {
90
+ describe: "Create a key that can access only this project. Its organization is looked up from the project",
91
+ type: "string",
92
+ coerce: single("project-id")
93
+ }
94
+ }).conflicts("org-id", "project-id").strict().check(noPassthrough("api-keys")), async (args) => await create(args)).command("revoke <id>", "Revoke an API key. Anything using it stops working immediately", (yargs) => yargs.positional("id", {
95
+ describe: "The API key id, from `api-keys list`",
96
+ type: "string",
97
+ demandOption: true,
98
+ coerce: (value) => {
99
+ const id = Number(value);
100
+ if (typeof value !== "string" || !/^\d+$/.test(value.trim()) || !Number.isSafeInteger(id) || id <= 0) throw new Error(`api-keys revoke needs a numeric key id, from \`neon api-keys list\`. Got \`${String(value)}\`.`);
101
+ return id;
102
+ }
103
+ }).options({ "org-id": {
104
+ describe: "Revoke an organization key instead of an account key",
105
+ type: "string",
106
+ coerce: single("org-id")
107
+ } }).strict().check(noPassthrough("api-keys")), async (args) => await revoke(args));
108
+ const handler = (args) => args;
109
+ const list = async (props) => {
110
+ const out = writer(props);
111
+ if (props.orgId) {
112
+ const { data } = await props.apiClient.listOrgApiKeys(props.orgId);
113
+ if (props.output === "table") out.write(data.map((key) => ({
114
+ ...key,
115
+ project: key.project_id ?? ALL_PROJECTS
116
+ })), {
117
+ fields: ORG_TABLE_FIELDS,
118
+ title: `API keys in ${props.orgId}`,
119
+ emptyMessage: `No API keys in ${props.orgId}.`
120
+ });
121
+ else out.write(data, {
122
+ fields: ORG_FIELDS,
123
+ title: `API keys in ${props.orgId}`,
124
+ emptyMessage: `No API keys in ${props.orgId}.`
125
+ });
126
+ out.end();
127
+ return;
128
+ }
129
+ const { data } = await props.apiClient.listApiKeys();
130
+ out.write(data, {
131
+ fields: ACCOUNT_FIELDS,
132
+ title: "Account API keys",
133
+ emptyMessage: "You have no account API keys."
134
+ });
135
+ out.end();
136
+ if (props.output === "table") log.info("Organization keys are listed separately: neon api-keys list --org-id <org> (see `neon orgs list`).");
137
+ };
138
+ const create = async (props) => {
139
+ const { name, projectId, orgId } = props;
140
+ if (!projectId && !orgId) {
141
+ const { data } = await props.apiClient.createApiKey({ key_name: name });
142
+ await assertUsable(props, data, {
143
+ orgId: null,
144
+ expect: NO_PROJECT
145
+ });
146
+ report(props, data, CREATE_FIELDS, CREATE_TABLE_FIELDS);
147
+ log.warning("This key reaches everything your account can, in every organization. Pass --org-id or --project-id to narrow it.");
148
+ return;
149
+ }
150
+ if (!projectId && orgId) {
151
+ const { data } = await props.apiClient.createOrgApiKey(orgId, { key_name: name });
152
+ await assertUsable(props, data, {
153
+ orgId,
154
+ expect: NO_PROJECT
155
+ });
156
+ report(props, data, CREATE_FIELDS, CREATE_TABLE_FIELDS);
157
+ log.warning("This key reaches every project in %s, including ones created later. Pass --project-id instead to restrict it to one.", orgId);
158
+ return;
159
+ }
160
+ const scopeTo = projectId;
161
+ const resolvedOrgId = await orgIdForProject(props.apiClient, scopeTo);
162
+ const { data } = await props.apiClient.createOrgApiKey(resolvedOrgId, {
163
+ key_name: name,
164
+ project_id: scopeTo
165
+ });
166
+ await assertUsable(props, data, {
167
+ orgId: resolvedOrgId,
168
+ expect: scopeTo
169
+ });
170
+ report(props, data, CREATE_FIELDS_SCOPED, CREATE_TABLE_FIELDS_SCOPED);
171
+ 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);
172
+ };
173
+ /**
174
+ * The key is shown only once, so human output keeps it on a selectable line.
175
+ * Structured output keeps it in the object for scripts.
176
+ */
177
+ const report = (props, data, fields, tableFields) => {
178
+ const out = writer(props);
179
+ if (props.output === "table") {
180
+ const row = typeof data.project_id === "string" ? {
181
+ ...data,
182
+ project: data.project_id
183
+ } : data;
184
+ out.write(row, {
185
+ fields: tableFields,
186
+ title: "API key"
187
+ });
188
+ out.end();
189
+ out.text(`\n${data.key}\n`);
190
+ } else {
191
+ out.write(data, {
192
+ fields,
193
+ title: "API key"
194
+ });
195
+ out.end();
196
+ }
197
+ log.warning("Store this key now: it is not shown again.");
198
+ };
199
+ /**
200
+ * Refuse to report a key that isn't what we asked for, and take it back.
201
+ *
202
+ * Two ways a 2xx can still be wrong: no `key` in the body, which leaves a live credential
203
+ * the user can never see or use; and a `project_id` that doesn't match the requested
204
+ * project, which would mean announcing a scope the key does not have. Both withdraw the key
205
+ * before throwing.
206
+ *
207
+ * The thrown message states whether the withdrawal actually succeeded. Saying "the key has
208
+ * been revoked" when the revoke itself failed would be worse than saying nothing — it would
209
+ * leave an unverified credential live while telling the user it is gone.
210
+ */
211
+ const assertUsable = async (props, data, scope) => {
212
+ const wanted = scope.expect === NO_PROJECT ? void 0 : scope.expect;
213
+ const problem = typeof data.key !== "string" || data.key.trim() === "" ? "Neon returned no key." : data.project_id !== wanted ? `Neon returned a key scoped to ${data.project_id ?? "nothing"} rather than ${wanted ?? "the whole organization"}.` : null;
214
+ if (!problem) return;
215
+ const withdrawn = await withdraw(props, scope.orgId, data.id);
216
+ throw new Error(`${problem} ${withdrawn ? "The key has been revoked; nothing was issued." : `The key could NOT be revoked and may still be live${data.id === void 0 ? "" : `. Remove it with \`neon api-keys revoke ${data.id}${scope.orgId ? ` --org-id ${scope.orgId}` : ""}\``}.`}`);
217
+ };
218
+ /**
219
+ * Revoke, turning the most likely mistake into a usable message.
220
+ *
221
+ * Account and organization keys live on different endpoints, and `api-keys list --org-id X`
222
+ * shows ids that the account endpoint cannot see. Copying one and forgetting the flag — or
223
+ * leaving it on for an account key — is the easy error, and a bare "API key not found" sends
224
+ * the user looking for a deleted key rather than a misplaced flag.
225
+ *
226
+ * Only the key id is worth second-guessing here: an org id that is wrong or not yours does
227
+ * not reach this path at all, because the API answers "not an organization member" (verified
228
+ * against production) rather than a 404.
229
+ */
230
+ const revokeOrExplain = async (props) => {
231
+ try {
232
+ return props.orgId ? await props.apiClient.revokeOrgApiKey(props.orgId, props.id) : await props.apiClient.revokeApiKey(props.id);
233
+ } catch (err) {
234
+ if (isNeonApiError(err) && err.status === 404) throw new Error(props.orgId ? `No API key with id ${props.id} in ${props.orgId}. If it is one of your account's own keys, drop --org-id.` : `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.`);
235
+ throw err;
236
+ }
237
+ };
238
+ /** Best-effort withdrawal of a key we are refusing to report. Never throws. */
239
+ const withdraw = async (props, orgId, keyId) => {
240
+ if (!Number.isSafeInteger(keyId) || keyId <= 0) return false;
241
+ try {
242
+ const { data } = orgId ? await props.apiClient.revokeOrgApiKey(orgId, keyId) : await props.apiClient.revokeApiKey(keyId);
243
+ return data.revoked === true && data.id === keyId;
244
+ } catch (err) {
245
+ log.warning("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
246
+ return false;
247
+ }
248
+ };
249
+ const revoke = async (props) => {
250
+ const out = writer(props);
251
+ const { data } = await revokeOrExplain(props);
252
+ out.write(data, {
253
+ fields: [
254
+ "id",
255
+ "name",
256
+ "revoked",
257
+ "last_used_at"
258
+ ],
259
+ title: "API key"
260
+ });
261
+ out.end();
262
+ };
263
+ /**
264
+ * The organization a project belongs to. A project outside any organization cannot have a
265
+ * scoped key — the endpoint that accepts `project_id` is org-only — so that case fails here
266
+ * with the reason, rather than as a 404 from a URL the user never typed.
267
+ */
268
+ const orgIdForProject = async (client, projectId, usage = "api-keys") => {
269
+ let orgId;
270
+ try {
271
+ const { data: { project } } = await client.getProject(projectId);
272
+ orgId = project.org_id;
273
+ } catch (err) {
274
+ if (isNeonApiError(err) && err.status === 404) throw new Error(projectId.startsWith("org-") ? usage === "mcp" ? `Project ${projectId} not found. That looks like an organization id. neon mcp takes a project id on --project-id.` : `Project ${projectId} not found. That looks like an organization id. Pass it as --org-id instead.` : `Project ${projectId} not found. Check the id with \`neon projects list\`.`);
275
+ throw err;
276
+ }
277
+ if (!orgId) throw new Error(usage === "mcp" ? `Project ${projectId} does not belong to an organization, so it cannot have a project-scoped API key. Pass --oauth to pin tools without minting, or omit --project-id to mint an account-wide key.` : `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.`);
278
+ return orgId;
279
+ };
280
+ //#endregion
281
+ export { describe as a, command as i, api_keys_exports as n, handler as o, builder as r, orgIdForProject as s, aliases as t };