neonctl 2.37.0 → 2.38.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.
Files changed (160) hide show
  1. package/{LICENSE.md → LICENSE} +27 -3
  2. package/README.md +15 -700
  3. package/bin/cli.js +3 -0
  4. package/package.json +8 -75
  5. package/dist/analytics.js +0 -168
  6. package/dist/api.js +0 -720
  7. package/dist/auth.js +0 -125
  8. package/dist/callback.html +0 -51
  9. package/dist/cli.js +0 -9
  10. package/dist/commands/api.js +0 -278
  11. package/dist/commands/auth.js +0 -214
  12. package/dist/commands/bootstrap.js +0 -481
  13. package/dist/commands/branches.js +0 -488
  14. package/dist/commands/bucket.js +0 -549
  15. package/dist/commands/checkout.js +0 -321
  16. package/dist/commands/config.js +0 -626
  17. package/dist/commands/connection_string.js +0 -172
  18. package/dist/commands/data_api.js +0 -284
  19. package/dist/commands/databases.js +0 -82
  20. package/dist/commands/deploy.js +0 -26
  21. package/dist/commands/dev.js +0 -698
  22. package/dist/commands/diff.js +0 -222
  23. package/dist/commands/env.js +0 -189
  24. package/dist/commands/functions.js +0 -373
  25. package/dist/commands/index.js +0 -62
  26. package/dist/commands/init.js +0 -73
  27. package/dist/commands/inspect.js +0 -65
  28. package/dist/commands/ip_allow.js +0 -137
  29. package/dist/commands/link.js +0 -1121
  30. package/dist/commands/neon_auth.js +0 -1028
  31. package/dist/commands/operations.js +0 -28
  32. package/dist/commands/orgs.js +0 -24
  33. package/dist/commands/projects.js +0 -413
  34. package/dist/commands/psql.js +0 -62
  35. package/dist/commands/roles.js +0 -65
  36. package/dist/commands/schema_diff.js +0 -151
  37. package/dist/commands/set_context.js +0 -29
  38. package/dist/commands/snapshots.js +0 -455
  39. package/dist/commands/status.js +0 -40
  40. package/dist/commands/user.js +0 -15
  41. package/dist/commands/vpc_endpoints.js +0 -134
  42. package/dist/config.js +0 -11
  43. package/dist/config_format.js +0 -72
  44. package/dist/context.js +0 -231
  45. package/dist/current_branch_fast_path.js +0 -55
  46. package/dist/dev/env.js +0 -244
  47. package/dist/dev/functions.js +0 -70
  48. package/dist/dev/inputs.js +0 -63
  49. package/dist/dev/runtime.js +0 -146
  50. package/dist/env.js +0 -36
  51. package/dist/env_file.js +0 -159
  52. package/dist/errors.js +0 -80
  53. package/dist/functions_api.js +0 -48
  54. package/dist/help.js +0 -146
  55. package/dist/index.js +0 -240
  56. package/dist/log.js +0 -18
  57. package/dist/parameters.gen.js +0 -480
  58. package/dist/pkg.js +0 -25
  59. package/dist/psql/cli.js +0 -53
  60. package/dist/psql/command/cmd_cond.js +0 -437
  61. package/dist/psql/command/cmd_connect.js +0 -820
  62. package/dist/psql/command/cmd_copy.js +0 -1035
  63. package/dist/psql/command/cmd_describe.js +0 -1815
  64. package/dist/psql/command/cmd_format.js +0 -948
  65. package/dist/psql/command/cmd_io.js +0 -2193
  66. package/dist/psql/command/cmd_lo.js +0 -393
  67. package/dist/psql/command/cmd_meta.js +0 -969
  68. package/dist/psql/command/cmd_misc.js +0 -187
  69. package/dist/psql/command/cmd_pipeline.js +0 -1148
  70. package/dist/psql/command/cmd_restrict.js +0 -171
  71. package/dist/psql/command/cmd_show.js +0 -766
  72. package/dist/psql/command/dispatch.js +0 -343
  73. package/dist/psql/command/inputQueue.js +0 -42
  74. package/dist/psql/command/shared.js +0 -71
  75. package/dist/psql/complete/filenames.js +0 -139
  76. package/dist/psql/complete/index.js +0 -104
  77. package/dist/psql/complete/matcher.js +0 -315
  78. package/dist/psql/complete/psqlVars.js +0 -249
  79. package/dist/psql/complete/queries.js +0 -493
  80. package/dist/psql/complete/rules.js +0 -2424
  81. package/dist/psql/core/common.js +0 -1253
  82. package/dist/psql/core/help.js +0 -576
  83. package/dist/psql/core/mainloop.js +0 -1360
  84. package/dist/psql/core/prompt.js +0 -439
  85. package/dist/psql/core/settings.js +0 -686
  86. package/dist/psql/core/sqlHelp.js +0 -1066
  87. package/dist/psql/core/startup.js +0 -846
  88. package/dist/psql/core/syncVars.js +0 -116
  89. package/dist/psql/core/variables.js +0 -287
  90. package/dist/psql/describe/formatters.js +0 -1330
  91. package/dist/psql/describe/processNamePattern.js +0 -270
  92. package/dist/psql/describe/queries.js +0 -2452
  93. package/dist/psql/describe/versionGate.js +0 -44
  94. package/dist/psql/index.js +0 -2030
  95. package/dist/psql/io/history.js +0 -299
  96. package/dist/psql/io/input.js +0 -120
  97. package/dist/psql/io/lineEditor/buffer.js +0 -325
  98. package/dist/psql/io/lineEditor/complete.js +0 -227
  99. package/dist/psql/io/lineEditor/filename.js +0 -159
  100. package/dist/psql/io/lineEditor/index.js +0 -893
  101. package/dist/psql/io/lineEditor/keymap.js +0 -745
  102. package/dist/psql/io/lineEditor/vt100.js +0 -363
  103. package/dist/psql/io/pgpass.js +0 -202
  104. package/dist/psql/io/pgservice.js +0 -194
  105. package/dist/psql/io/psqlrc.js +0 -422
  106. package/dist/psql/print/aligned.js +0 -1748
  107. package/dist/psql/print/asciidoc.js +0 -230
  108. package/dist/psql/print/crosstab.js +0 -463
  109. package/dist/psql/print/csv.js +0 -76
  110. package/dist/psql/print/html.js +0 -240
  111. package/dist/psql/print/json.js +0 -96
  112. package/dist/psql/print/latex.js +0 -379
  113. package/dist/psql/print/pager.js +0 -267
  114. package/dist/psql/print/troff.js +0 -240
  115. package/dist/psql/print/unaligned.js +0 -99
  116. package/dist/psql/print/units.js +0 -188
  117. package/dist/psql/scanner/slash.js +0 -515
  118. package/dist/psql/scanner/sql.js +0 -914
  119. package/dist/psql/scanner/stringutils.js +0 -394
  120. package/dist/psql/types/backslash.js +0 -1
  121. package/dist/psql/types/connection.js +0 -1
  122. package/dist/psql/types/index.js +0 -7
  123. package/dist/psql/types/printer.js +0 -1
  124. package/dist/psql/types/repl.js +0 -1
  125. package/dist/psql/types/scanner.js +0 -24
  126. package/dist/psql/types/settings.js +0 -1
  127. package/dist/psql/types/variables.js +0 -1
  128. package/dist/psql/wire/connection.js +0 -2857
  129. package/dist/psql/wire/copy.js +0 -108
  130. package/dist/psql/wire/notify.js +0 -59
  131. package/dist/psql/wire/pipeline.js +0 -521
  132. package/dist/psql/wire/protocol.js +0 -466
  133. package/dist/psql/wire/sasl.js +0 -294
  134. package/dist/psql/wire/tls.js +0 -602
  135. package/dist/storage_api.js +0 -147
  136. package/dist/test_utils/fixtures.js +0 -121
  137. package/dist/test_utils/oauth_server.js +0 -9
  138. package/dist/types.js +0 -1
  139. package/dist/utils/ai_gateway_notice.js +0 -180
  140. package/dist/utils/api_enums.js +0 -33
  141. package/dist/utils/auth.js +0 -5
  142. package/dist/utils/branch_notice.js +0 -22
  143. package/dist/utils/branch_picker.js +0 -103
  144. package/dist/utils/compute_units.js +0 -28
  145. package/dist/utils/config_diff.js +0 -185
  146. package/dist/utils/enrichers.js +0 -161
  147. package/dist/utils/esbuild.js +0 -158
  148. package/dist/utils/formats.js +0 -18
  149. package/dist/utils/git_diff.js +0 -90
  150. package/dist/utils/inspect_db.js +0 -153
  151. package/dist/utils/inspect_queries.js +0 -372
  152. package/dist/utils/middlewares.js +0 -20
  153. package/dist/utils/openapi.js +0 -114
  154. package/dist/utils/package_manager.js +0 -68
  155. package/dist/utils/point_in_time.js +0 -56
  156. package/dist/utils/psql.js +0 -120
  157. package/dist/utils/string.js +0 -5
  158. package/dist/utils/ui.js +0 -59
  159. package/dist/utils/zip.js +0 -4
  160. package/dist/writer.js +0 -97
package/dist/auth.js DELETED
@@ -1,125 +0,0 @@
1
- import { createReadStream } from "node:fs";
2
- import { createServer, } from "node:http";
3
- import { join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import open from "open";
6
- import * as client from "openid-client";
7
- import { sendError } from "./analytics.js";
8
- import { matchErrorCode } from "./errors.js";
9
- import { log } from "./log.js";
10
- import { extendTokenSet } from "./utils/auth.js";
11
- // oauth server timeouts
12
- const SERVER_TIMEOUT = 10000;
13
- // where to wait for incoming redirect request from oauth server to arrive
14
- const REDIRECT_URI = (port) => `http://127.0.0.1:${port}/callback`;
15
- // These scopes cannot be cancelled, they are always needed.
16
- const ALWAYS_PRESENT_SCOPES = ["openid", "offline", "offline_access"];
17
- const NEONCTL_SCOPES = [
18
- ...ALWAYS_PRESENT_SCOPES,
19
- "urn:neoncloud:projects:create",
20
- "urn:neoncloud:projects:read",
21
- "urn:neoncloud:projects:update",
22
- "urn:neoncloud:projects:delete",
23
- "urn:neoncloud:orgs:create",
24
- "urn:neoncloud:orgs:read",
25
- "urn:neoncloud:orgs:update",
26
- "urn:neoncloud:orgs:delete",
27
- "urn:neoncloud:orgs:permission",
28
- ];
29
- const AUTH_TIMEOUT_SECONDS = 60;
30
- export const defaultClientID = "neonctl";
31
- export const refreshToken = async ({ oauthHost, clientId, allowUnsafeTls }, tokenSet) => {
32
- log.debug("Discovering oauth server");
33
- const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
34
- timeout: SERVER_TIMEOUT,
35
- execute: allowUnsafeTls
36
- ? [client.allowInsecureRequests]
37
- : undefined,
38
- });
39
- return await client.refreshTokenGrant(configuration, tokenSet.refresh_token);
40
- };
41
- export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
42
- log.debug("Discovering oauth server");
43
- const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
44
- timeout: SERVER_TIMEOUT,
45
- execute: allowUnsafeTls
46
- ? [client.allowInsecureRequests]
47
- : undefined,
48
- });
49
- //
50
- // Start HTTP server and wait till /callback is hit
51
- //
52
- log.debug("Starting HTTP Server for callback");
53
- const server = createServer();
54
- server.listen(0, "127.0.0.1", function () {
55
- log.debug(`Listening on port ${this.address().port}`);
56
- });
57
- await new Promise((resolve) => server.once("listening", resolve));
58
- const listen_port = server.address().port;
59
- // https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
60
- const state = client.randomState();
61
- // we store the code_verifier in memory
62
- const codeVerifier = client.randomPKCECodeVerifier();
63
- const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
64
- return new Promise((resolve, reject) => {
65
- const timer = setTimeout(() => {
66
- reject(new Error(`Authentication timed out after ${AUTH_TIMEOUT_SECONDS} seconds`));
67
- }, AUTH_TIMEOUT_SECONDS * 1000);
68
- const onRequest = async (request, response) => {
69
- //
70
- // Wait for callback and follow oauth flow.
71
- //
72
- if (!request.url?.startsWith("/callback")) {
73
- response.writeHead(404);
74
- response.end();
75
- return;
76
- }
77
- // process the CORS preflight OPTIONS request
78
- if (request.method === "OPTIONS") {
79
- response.writeHead(200, {
80
- "Access-Control-Allow-Origin": "*",
81
- "Access-Control-Allow-Methods": "GET, POST",
82
- "Access-Control-Allow-Headers": "Content-Type",
83
- });
84
- response.end();
85
- return;
86
- }
87
- log.debug(`Callback received: ${request.url}`);
88
- const tokenSet = await client.authorizationCodeGrant(configuration, new URL(request.url, `http://127.0.0.1:${listen_port}`), {
89
- pkceCodeVerifier: codeVerifier,
90
- expectedState: state,
91
- });
92
- response.writeHead(200, { "Content-Type": "text/html" });
93
- createReadStream(join(fileURLToPath(new URL(".", import.meta.url)), "./callback.html")).pipe(response);
94
- clearTimeout(timer);
95
- const exp = new Date();
96
- exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
97
- resolve(extendTokenSet(tokenSet));
98
- server.close();
99
- };
100
- server.on("request", (req, res) => {
101
- void onRequest(req, res);
102
- });
103
- //
104
- // Open browser to let user authenticate
105
- //
106
- const scopes = clientId == defaultClientID
107
- ? NEONCTL_SCOPES
108
- : ALWAYS_PRESENT_SCOPES;
109
- const authUrl = client.buildAuthorizationUrl(configuration, {
110
- scope: scopes.join(" "),
111
- state,
112
- code_challenge: codeChallenge,
113
- code_challenge_method: "S256",
114
- redirect_uri: REDIRECT_URI(listen_port),
115
- });
116
- log.info("Awaiting authentication in web browser.");
117
- log.info(`Auth Url: ${authUrl}`);
118
- open(authUrl.href).catch((err) => {
119
- const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
120
- const typedErr = err && err instanceof Error ? err : undefined;
121
- sendError(typedErr || new Error(msg), matchErrorCode(msg));
122
- log.error(msg);
123
- });
124
- });
125
- };
@@ -1,51 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <title>Neon</title>
6
- <style>
7
- body,
8
- html {
9
- width: 100%;
10
- height: 100%;
11
- margin: 0;
12
- text-align: center;
13
- font-family: 'Open Sans', sans-serif;
14
- display: flex;
15
- flex-direction: column;
16
- justify-content: center;
17
- align-items: center;
18
- background-color: #ffffff;
19
- color: #2d374c;
20
- }
21
- @media (prefers-color-scheme: dark) {
22
- body,
23
- html {
24
- background-color: #191919;
25
- color: #bfbfbf;
26
- }
27
- }
28
- .logo {
29
- display: inline-block;
30
- width: 100px;
31
- height: 100px;
32
- margin: 0 auto;
33
- }
34
- svg {
35
- overflow: visible;
36
- }
37
- </style>
38
- </head>
39
- <body>
40
- <div class="logo">
41
- <svg viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
42
- <path
43
- d="M58 0.0162365V58L35.3688 38.5587V58H0V0L58 0.0162365ZM7.10962 50.9603H28.2591V23.1112L50.8907 42.937V7.05391L7.10962 7.04147V50.9603Z"
44
- fill="#34D59A"
45
- />
46
- </svg>
47
- </div>
48
- <h1>Thank you for using Neon</h1>
49
- <p>You may close this page now</p>
50
- </body>
51
- </html>
package/dist/cli.js DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import { tryCurrentBranchFastPath } from "./current_branch_fast_path.js";
3
- // Fast path for the offline `(config) status --current-branch` probe (used by shell
4
- // prompts): read the pinned branch from `.neon` without loading the full command tree,
5
- // api-client, and yargs (~200ms). Falls through to the full CLI for everything else, so
6
- // the heavy `index.js` is imported lazily and only when actually needed.
7
- if (!tryCurrentBranchFastPath(process.argv)) {
8
- void import("./index.js");
9
- }
@@ -1,278 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import YAML from "yaml";
4
- import { DEFAULT_SPEC_URL, getEndpoints, loadSpec } from "../utils/openapi.js";
5
- import { writer } from "../writer.js";
6
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
7
- // ── Pure helpers (unit-tested) ───────────────────────────────────────────────
8
- /**
9
- * Coerce a raw string into a typed JSON value: booleans, `null`, integers,
10
- * floats, and JSON arrays/objects are parsed; everything else stays a string.
11
- */
12
- export function parseTypedValue(raw) {
13
- if (raw === "true")
14
- return true;
15
- if (raw === "false")
16
- return false;
17
- if (raw === "null")
18
- return null;
19
- if (/^-?\d+$/.test(raw))
20
- return Number.parseInt(raw, 10);
21
- if (/^-?\d*\.\d+$/.test(raw))
22
- return Number.parseFloat(raw);
23
- if (raw.startsWith("[") || raw.startsWith("{")) {
24
- try {
25
- return JSON.parse(raw);
26
- }
27
- catch {
28
- return raw;
29
- }
30
- }
31
- return raw;
32
- }
33
- /** Split a `key=value` pair on the first `=`. */
34
- export function parseKeyValue(input) {
35
- const eq = input.indexOf("=");
36
- if (eq === -1) {
37
- throw new Error(`Invalid "key=value" pair: "${input}".`);
38
- }
39
- return { key: input.slice(0, eq), value: input.slice(eq + 1) };
40
- }
41
- /**
42
- * Assign `value` into `target` at a dot-delimited path, creating intermediate
43
- * objects as needed. Enables `-F branch.name=dev` → `{ branch: { name: "dev" } }`,
44
- * matching the nested shape of Neon request bodies.
45
- */
46
- export function setDeep(target, dottedKey, value) {
47
- const parts = dottedKey.split(".");
48
- let node = target;
49
- for (let i = 0; i < parts.length - 1; i++) {
50
- const part = parts[i];
51
- const next = node[part];
52
- if (typeof next !== "object" || next === null || Array.isArray(next)) {
53
- node[part] = {};
54
- }
55
- node = node[part];
56
- }
57
- node[parts[parts.length - 1]] = value;
58
- }
59
- /** Build a JSON body object from typed `-F` and string `-f` field pairs. */
60
- export function buildBody(fields, rawFields) {
61
- const body = {};
62
- for (const field of fields) {
63
- const { key, value } = parseKeyValue(field);
64
- setDeep(body, key, parseTypedValue(value));
65
- }
66
- for (const field of rawFields) {
67
- const { key, value } = parseKeyValue(field);
68
- setDeep(body, key, value);
69
- }
70
- return body;
71
- }
72
- /** Build a query-string map from `-Q key=value` pairs. */
73
- export function buildQuery(pairs) {
74
- const query = {};
75
- for (const pair of pairs) {
76
- const { key, value } = parseKeyValue(pair);
77
- query[key] = value;
78
- }
79
- return query;
80
- }
81
- /** Build a header map from `-H key:value` pairs. */
82
- export function parseHeaders(pairs) {
83
- const headers = {};
84
- for (const pair of pairs) {
85
- const idx = pair.indexOf(":");
86
- if (idx === -1) {
87
- throw new Error(`Invalid header "${pair}". Expected "key:value".`);
88
- }
89
- headers[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
90
- }
91
- return headers;
92
- }
93
- function assertMethod(method) {
94
- if (!HTTP_METHODS.includes(method)) {
95
- throw new Error(`Unsupported method "${method}". Use one of: ${HTTP_METHODS.join(", ")}.`);
96
- }
97
- }
98
- // ── I/O helpers ──────────────────────────────────────────────────────────────
99
- async function readStdin() {
100
- const chunks = [];
101
- for await (const chunk of process.stdin) {
102
- chunks.push(Buffer.from(chunk));
103
- }
104
- return Buffer.concat(chunks).toString("utf8");
105
- }
106
- /** Resolve `--data`: `-` reads stdin, `@file` reads a file, else the literal string. Parsed as JSON when possible. */
107
- async function readData(data) {
108
- let raw;
109
- if (data === "-") {
110
- raw = await readStdin();
111
- }
112
- else if (data.startsWith("@")) {
113
- raw = readFileSync(resolve(data.slice(1)), "utf8");
114
- }
115
- else {
116
- raw = data;
117
- }
118
- try {
119
- return JSON.parse(raw);
120
- }
121
- catch {
122
- return raw;
123
- }
124
- }
125
- const toStrings = (values) => (values ?? []).map(String);
126
- async function listEndpoints(args) {
127
- const spec = await loadSpec({
128
- configDir: args.configDir,
129
- specUrl: args.specUrl,
130
- refresh: args.refresh,
131
- });
132
- if (!spec) {
133
- throw new Error(`Could not load the Neon OpenAPI spec from ${args.specUrl}. ` +
134
- "Check your network connection or pass --spec-url.");
135
- }
136
- const endpoints = getEndpoints(spec).map((endpoint) => ({
137
- method: endpoint.method,
138
- path: endpoint.path,
139
- summary: endpoint.summary ?? "",
140
- }));
141
- writer(args).end(endpoints, {
142
- fields: ["method", "path", "summary"],
143
- title: `Neon API endpoints (${endpoints.length})`,
144
- emptyMessage: "No endpoints found in the spec.",
145
- });
146
- }
147
- async function runRequest(args) {
148
- const path = args.path;
149
- if (!path) {
150
- throw new Error("Missing API path. Usage: neon api <path> (e.g. neon api /projects). " +
151
- "Run `neon api --list` to see available routes.");
152
- }
153
- if (!path.startsWith("/")) {
154
- throw new Error(`Invalid path "${path}". API paths must start with "/". ` +
155
- "Run `neon api --list` to see available routes.");
156
- }
157
- const fields = toStrings(args.field);
158
- const rawFields = toStrings(args.rawField);
159
- let body;
160
- if (args.data !== undefined) {
161
- body = await readData(args.data);
162
- }
163
- else if (fields.length > 0 || rawFields.length > 0) {
164
- body = buildBody(fields, rawFields);
165
- }
166
- const hasBody = body !== undefined;
167
- const method = String(args.method ?? (hasBody ? "POST" : "GET")).toUpperCase();
168
- assertMethod(method);
169
- const query = buildQuery(toStrings(args.query));
170
- const headers = parseHeaders(toStrings(args.header));
171
- const params = {
172
- path,
173
- method,
174
- ...(Object.keys(query).length > 0 ? { query } : {}),
175
- ...(hasBody ? { body } : {}),
176
- ...(Object.keys(headers).length > 0 ? { headers } : {}),
177
- };
178
- const response = await args.apiClient.request(params);
179
- const out = process.stdout;
180
- if (args.include) {
181
- out.write(`HTTP ${response.status} ${response.statusText}\n`);
182
- for (const [key, value] of Object.entries(response.headers)) {
183
- out.write(`${key}: ${value}\n`);
184
- }
185
- out.write("\n");
186
- }
187
- if (response.data === undefined) {
188
- return;
189
- }
190
- if (args.output === "yaml") {
191
- out.write(YAML.stringify(response.data));
192
- }
193
- else {
194
- out.write(`${JSON.stringify(response.data, null, 2)}\n`);
195
- }
196
- }
197
- export const command = "api [path]";
198
- export const describe = "Call any Neon API route directly (authenticated passthrough)";
199
- export const builder = (argv) => argv
200
- .usage("$0 api <path> [options]")
201
- .positional("path", {
202
- type: "string",
203
- describe: 'API path beginning with "/" (e.g. /projects or ' +
204
- "/projects/{project_id}/branches). Use `list` to list endpoints.",
205
- })
206
- .options({
207
- method: {
208
- alias: "X",
209
- type: "string",
210
- describe: "HTTP method (GET, POST, PUT, PATCH, DELETE). " +
211
- "Defaults to GET, or POST when a body is provided.",
212
- },
213
- field: {
214
- alias: "F",
215
- type: "array",
216
- string: true,
217
- describe: "Body field key=value (repeatable). Dot-notation nests " +
218
- "objects (e.g. -F branch.name=dev); values are typed " +
219
- "(numbers, booleans, null, JSON).",
220
- },
221
- "raw-field": {
222
- alias: "f",
223
- type: "array",
224
- string: true,
225
- describe: "Body field key=value with the value kept as a raw string.",
226
- },
227
- data: {
228
- alias: "d",
229
- type: "string",
230
- describe: "Raw request body: a JSON string, @file, or - for stdin. " +
231
- "Overrides --field.",
232
- },
233
- query: {
234
- alias: "Q",
235
- type: "array",
236
- string: true,
237
- describe: "Query parameter key=value (repeatable).",
238
- },
239
- header: {
240
- alias: "H",
241
- type: "array",
242
- string: true,
243
- describe: "Extra request header key:value (repeatable).",
244
- },
245
- include: {
246
- alias: "i",
247
- type: "boolean",
248
- default: false,
249
- describe: "Print the response status and headers before the body.",
250
- },
251
- list: {
252
- type: "boolean",
253
- default: false,
254
- describe: "List available API endpoints from the OpenAPI spec.",
255
- },
256
- refresh: {
257
- type: "boolean",
258
- default: false,
259
- describe: "Refresh the cached OpenAPI spec (used with --list).",
260
- },
261
- "spec-url": {
262
- type: "string",
263
- default: process.env.NEON_API_SPEC_URL ?? DEFAULT_SPEC_URL,
264
- hidden: true,
265
- describe: "OpenAPI spec URL used by --list.",
266
- },
267
- })
268
- .example("$0 api /projects", "List your projects")
269
- .example("$0 api /projects/{id}/branches -X POST -F branch.name=dev", "Create a branch")
270
- .example("$0 api --list", "List every available API route");
271
- export const handler = async (args) => {
272
- const apiArgs = args;
273
- if (apiArgs.list || apiArgs.path === "list" || apiArgs.path === "ls") {
274
- await listEndpoints(apiArgs);
275
- return;
276
- }
277
- await runRequest(apiArgs);
278
- };
@@ -1,214 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { getApiClient } from "../api.js";
5
- import { auth, refreshToken } from "../auth.js";
6
- import { CREDENTIALS_FILE } from "../config.js";
7
- import { isConfigInit, isCurrentBranchProbe } from "../context.js";
8
- import { isCi } from "../env.js";
9
- import { log } from "../log.js";
10
- import { extendTokenSet } from "../utils/auth.js";
11
- export const command = "auth";
12
- export const aliases = ["login"];
13
- export const describe = "Authenticate";
14
- export const builder = (yargs) => yargs.option("context-file", {
15
- hidden: true,
16
- });
17
- export const handler = async (args) => {
18
- await authFlow(args);
19
- };
20
- export const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, "force-auth": forceAuthKebab, allowUnsafeTls, }) => {
21
- const allowInteractiveAuth = forceAuth ?? forceAuthKebab;
22
- if (!allowInteractiveAuth && isCi()) {
23
- throw new Error("Cannot run interactive auth in CI");
24
- }
25
- const tokenSet = await auth({
26
- oauthHost: oauthHost,
27
- clientId: clientId,
28
- allowUnsafeTls,
29
- });
30
- const credentialsPath = join(configDir, CREDENTIALS_FILE);
31
- try {
32
- await preserveCredentials(credentialsPath, tokenSet, getApiClient({
33
- apiKey: tokenSet.access_token || "",
34
- apiHost,
35
- }));
36
- }
37
- catch {
38
- log.error("Failed to save credentials");
39
- return "";
40
- }
41
- log.info("Auth complete");
42
- return tokenSet.access_token || "";
43
- };
44
- const preserveCredentials = async (path, credentials, apiClient) => {
45
- const { data: { id }, } = await apiClient.getCurrentUserInfo();
46
- const contents = JSON.stringify({
47
- // Cast to a plain record: we intentionally spread the credentials object.
48
- ...credentials,
49
- user_id: id,
50
- });
51
- // correctly sets needed permissions for the credentials file
52
- writeFileSync(path, contents, {
53
- mode: 0o700,
54
- });
55
- log.debug("Saved credentials to %s", path);
56
- log.debug("Credentials MD5 hash: %s", md5hash(contents));
57
- };
58
- const handleExistingToken = async (tokenSet, props, credentialsPath) => {
59
- // Use existing access_token, if present and valid
60
- if (tokenSet.access_token && tokenSet.expires_at > Date.now()) {
61
- log.debug("Using existing valid access_token");
62
- const apiClient = getApiClient({
63
- apiKey: tokenSet.access_token,
64
- apiHost: props.apiHost,
65
- });
66
- return { apiKey: tokenSet.access_token, apiClient };
67
- }
68
- // Either access_token is missing or its expired. Refresh the token
69
- log.debug(tokenSet.expires_at < Date.now()
70
- ? "Token is expired, attempting refresh"
71
- : "Token is missing access_token, attempting refresh");
72
- if (!tokenSet.refresh_token) {
73
- log.debug("TokenSet is missing refresh_token, starting authentication");
74
- return null;
75
- }
76
- try {
77
- const refreshedTokenSet = await refreshToken({
78
- oauthHost: props.oauthHost,
79
- clientId: props.clientId,
80
- allowUnsafeTls: props.allowUnsafeTls,
81
- }, tokenSet);
82
- // Extend the token set with expires_at
83
- const extendedTokenSet = extendTokenSet(refreshedTokenSet);
84
- const apiKey = extendedTokenSet.access_token;
85
- const apiClient = getApiClient({
86
- apiKey,
87
- apiHost: props.apiHost,
88
- });
89
- await preserveCredentials(credentialsPath, extendedTokenSet, apiClient);
90
- log.debug("Token refresh successful");
91
- return { apiKey, apiClient };
92
- }
93
- catch (err) {
94
- const typedErr = err instanceof Error ? err : new Error("Unknown error");
95
- log.debug("Failed to refresh token: %s", typedErr.message);
96
- throw new Error("AUTH_REFRESH_FAILED");
97
- }
98
- };
99
- export const ensureAuth = async (props) => {
100
- // Skip auth for help command or no command
101
- if (props._.length === 0 || props.help) {
102
- return;
103
- }
104
- // `(config) status --current-branch` is a purely-local read of `.neon`; it must
105
- // never refresh a token or pop a browser login. Skip auth entirely (the handler
106
- // doesn't use an API client in this mode).
107
- if (isCurrentBranchProbe(props)) {
108
- return;
109
- }
110
- // `config init` only scaffolds a neon.ts and installs npm packages locally; it
111
- // never calls the Neon API, so skip auth entirely — no token refresh, no login.
112
- if (isConfigInit(props)) {
113
- return;
114
- }
115
- // `dev` runs a function locally. It injects the selected branch's env vars
116
- // when credentials happen to be available, but must never trigger an
117
- // interactive login: use an API key or existing stored credentials if
118
- // present, otherwise run with no API client (env injection is skipped).
119
- const isLocalDev = props._[0] === "dev";
120
- // `bootstrap` only copies a public template repo; it never calls the Neon
121
- // API, so it must work without credentials and must never pop a browser
122
- // login. It uses an API key / stored credentials when present (harmless),
123
- // otherwise it proceeds with no API client.
124
- const isBootstrap = props._[0] === "bootstrap";
125
- // `init` manages its own auth flow (asks the user if they have an account,
126
- // then triggers OAuth at the right time). Skip the global auth middleware.
127
- const isInit = props._[0] === "init";
128
- // Use existing API key or handle auth command
129
- if (props.apiKey || props._[0] === "auth") {
130
- if (props.apiKey) {
131
- log.debug("Using an API key to authorize requests");
132
- }
133
- props.apiClient = getApiClient({
134
- apiKey: props.apiKey,
135
- apiHost: props.apiHost,
136
- });
137
- return;
138
- }
139
- const credentialsPath = join(props.configDir, CREDENTIALS_FILE);
140
- // Handle case when credentials file exists
141
- if (existsSync(credentialsPath)) {
142
- log.debug("Trying to read credentials from %s", credentialsPath);
143
- try {
144
- const contents = readFileSync(credentialsPath, "utf8");
145
- log.debug("Credentials MD5 hash: %s", md5hash(contents));
146
- const tokenSet = JSON.parse(contents);
147
- // Try to use existing token or refresh it
148
- const result = await handleExistingToken(tokenSet, props, credentialsPath);
149
- if (result) {
150
- props.apiKey = result.apiKey;
151
- props.apiClient = result.apiClient;
152
- return;
153
- }
154
- }
155
- catch (err) {
156
- if (!(err instanceof Error &&
157
- err.message === "AUTH_REFRESH_FAILED") &&
158
- err.code !== "ENOENT" &&
159
- !(err instanceof SyntaxError)) {
160
- // Throw for any errors except auth refresh failure, missing file, or invalid credentials file
161
- throw err;
162
- }
163
- // Fall through to new auth flow for auth failures
164
- log.debug("Ensure auth failed, starting authentication", err);
165
- }
166
- }
167
- else {
168
- log.debug("Credentials file %s does not exist, starting authentication", credentialsPath);
169
- }
170
- // `dev` never launches the interactive browser flow. With no usable
171
- // credentials it proceeds without an API client; env injection is skipped
172
- // and the function still runs locally.
173
- if (isLocalDev) {
174
- log.debug("dev: no usable credentials; running without env injection");
175
- return;
176
- }
177
- if (isBootstrap) {
178
- log.debug("bootstrap: no usable credentials; continuing without auth");
179
- return;
180
- }
181
- if (isInit) {
182
- log.debug("init: skipping global auth; init manages its own auth flow");
183
- return;
184
- }
185
- // Start new auth flow if no valid token exists or refresh failed
186
- const apiKey = await authFlow(props);
187
- props.apiKey = apiKey;
188
- props.apiClient = getApiClient({
189
- apiKey,
190
- apiHost: props.apiHost,
191
- });
192
- };
193
- /**
194
- * Deletes the credentials file at the specified path
195
- * @param configDir Directory where credentials file is stored
196
- */
197
- export const deleteCredentials = (configDir) => {
198
- const credentialsPath = join(configDir, CREDENTIALS_FILE);
199
- try {
200
- if (existsSync(credentialsPath)) {
201
- rmSync(credentialsPath);
202
- log.info("Deleted credentials from %s", credentialsPath);
203
- }
204
- else {
205
- log.debug("Credentials file %s does not exist", credentialsPath);
206
- }
207
- }
208
- catch (err) {
209
- const typedErr = err instanceof Error ? err : new Error("Unknown error");
210
- log.error("Failed to delete credentials: %s", typedErr.message);
211
- throw new Error("CREDENTIALS_DELETE_FAILED");
212
- }
213
- };
214
- const md5hash = (s) => createHash("md5").update(s).digest("hex");