neonctl 2.37.1 → 2.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +15 -700
  2. package/bin/cli.js +3 -0
  3. package/package.json +8 -75
  4. package/dist/analytics.js +0 -168
  5. package/dist/api.js +0 -720
  6. package/dist/auth.js +0 -125
  7. package/dist/callback.html +0 -60
  8. package/dist/cli.js +0 -9
  9. package/dist/commands/api.js +0 -278
  10. package/dist/commands/auth.js +0 -214
  11. package/dist/commands/bootstrap.js +0 -481
  12. package/dist/commands/branches.js +0 -488
  13. package/dist/commands/bucket.js +0 -549
  14. package/dist/commands/checkout.js +0 -321
  15. package/dist/commands/config.js +0 -626
  16. package/dist/commands/connection_string.js +0 -172
  17. package/dist/commands/data_api.js +0 -284
  18. package/dist/commands/databases.js +0 -82
  19. package/dist/commands/deploy.js +0 -26
  20. package/dist/commands/dev.js +0 -698
  21. package/dist/commands/diff.js +0 -222
  22. package/dist/commands/env.js +0 -189
  23. package/dist/commands/functions.js +0 -373
  24. package/dist/commands/index.js +0 -62
  25. package/dist/commands/init.js +0 -73
  26. package/dist/commands/inspect.js +0 -65
  27. package/dist/commands/ip_allow.js +0 -137
  28. package/dist/commands/link.js +0 -1121
  29. package/dist/commands/neon_auth.js +0 -1028
  30. package/dist/commands/operations.js +0 -28
  31. package/dist/commands/orgs.js +0 -24
  32. package/dist/commands/projects.js +0 -413
  33. package/dist/commands/psql.js +0 -62
  34. package/dist/commands/roles.js +0 -65
  35. package/dist/commands/schema_diff.js +0 -151
  36. package/dist/commands/set_context.js +0 -29
  37. package/dist/commands/snapshots.js +0 -455
  38. package/dist/commands/status.js +0 -40
  39. package/dist/commands/user.js +0 -15
  40. package/dist/commands/vpc_endpoints.js +0 -134
  41. package/dist/config.js +0 -11
  42. package/dist/config_format.js +0 -72
  43. package/dist/context.js +0 -231
  44. package/dist/current_branch_fast_path.js +0 -55
  45. package/dist/dev/env.js +0 -244
  46. package/dist/dev/functions.js +0 -70
  47. package/dist/dev/inputs.js +0 -63
  48. package/dist/dev/runtime.js +0 -146
  49. package/dist/env.js +0 -36
  50. package/dist/env_file.js +0 -159
  51. package/dist/errors.js +0 -80
  52. package/dist/functions_api.js +0 -48
  53. package/dist/help.js +0 -146
  54. package/dist/index.js +0 -240
  55. package/dist/log.js +0 -18
  56. package/dist/parameters.gen.js +0 -480
  57. package/dist/pkg.js +0 -25
  58. package/dist/psql/cli.js +0 -53
  59. package/dist/psql/command/cmd_cond.js +0 -437
  60. package/dist/psql/command/cmd_connect.js +0 -820
  61. package/dist/psql/command/cmd_copy.js +0 -1035
  62. package/dist/psql/command/cmd_describe.js +0 -1815
  63. package/dist/psql/command/cmd_format.js +0 -948
  64. package/dist/psql/command/cmd_io.js +0 -2193
  65. package/dist/psql/command/cmd_lo.js +0 -393
  66. package/dist/psql/command/cmd_meta.js +0 -969
  67. package/dist/psql/command/cmd_misc.js +0 -187
  68. package/dist/psql/command/cmd_pipeline.js +0 -1148
  69. package/dist/psql/command/cmd_restrict.js +0 -171
  70. package/dist/psql/command/cmd_show.js +0 -766
  71. package/dist/psql/command/dispatch.js +0 -343
  72. package/dist/psql/command/inputQueue.js +0 -42
  73. package/dist/psql/command/shared.js +0 -71
  74. package/dist/psql/complete/filenames.js +0 -139
  75. package/dist/psql/complete/index.js +0 -104
  76. package/dist/psql/complete/matcher.js +0 -315
  77. package/dist/psql/complete/psqlVars.js +0 -249
  78. package/dist/psql/complete/queries.js +0 -493
  79. package/dist/psql/complete/rules.js +0 -2424
  80. package/dist/psql/core/common.js +0 -1253
  81. package/dist/psql/core/help.js +0 -576
  82. package/dist/psql/core/mainloop.js +0 -1360
  83. package/dist/psql/core/prompt.js +0 -439
  84. package/dist/psql/core/settings.js +0 -686
  85. package/dist/psql/core/sqlHelp.js +0 -1066
  86. package/dist/psql/core/startup.js +0 -846
  87. package/dist/psql/core/syncVars.js +0 -116
  88. package/dist/psql/core/variables.js +0 -287
  89. package/dist/psql/describe/formatters.js +0 -1330
  90. package/dist/psql/describe/processNamePattern.js +0 -270
  91. package/dist/psql/describe/queries.js +0 -2452
  92. package/dist/psql/describe/versionGate.js +0 -44
  93. package/dist/psql/index.js +0 -2030
  94. package/dist/psql/io/history.js +0 -299
  95. package/dist/psql/io/input.js +0 -120
  96. package/dist/psql/io/lineEditor/buffer.js +0 -325
  97. package/dist/psql/io/lineEditor/complete.js +0 -227
  98. package/dist/psql/io/lineEditor/filename.js +0 -159
  99. package/dist/psql/io/lineEditor/index.js +0 -893
  100. package/dist/psql/io/lineEditor/keymap.js +0 -745
  101. package/dist/psql/io/lineEditor/vt100.js +0 -363
  102. package/dist/psql/io/pgpass.js +0 -202
  103. package/dist/psql/io/pgservice.js +0 -194
  104. package/dist/psql/io/psqlrc.js +0 -422
  105. package/dist/psql/print/aligned.js +0 -1748
  106. package/dist/psql/print/asciidoc.js +0 -230
  107. package/dist/psql/print/crosstab.js +0 -463
  108. package/dist/psql/print/csv.js +0 -76
  109. package/dist/psql/print/html.js +0 -240
  110. package/dist/psql/print/json.js +0 -96
  111. package/dist/psql/print/latex.js +0 -379
  112. package/dist/psql/print/pager.js +0 -267
  113. package/dist/psql/print/troff.js +0 -240
  114. package/dist/psql/print/unaligned.js +0 -99
  115. package/dist/psql/print/units.js +0 -188
  116. package/dist/psql/scanner/slash.js +0 -515
  117. package/dist/psql/scanner/sql.js +0 -914
  118. package/dist/psql/scanner/stringutils.js +0 -394
  119. package/dist/psql/types/backslash.js +0 -1
  120. package/dist/psql/types/connection.js +0 -1
  121. package/dist/psql/types/index.js +0 -7
  122. package/dist/psql/types/printer.js +0 -1
  123. package/dist/psql/types/repl.js +0 -1
  124. package/dist/psql/types/scanner.js +0 -24
  125. package/dist/psql/types/settings.js +0 -1
  126. package/dist/psql/types/variables.js +0 -1
  127. package/dist/psql/wire/connection.js +0 -2857
  128. package/dist/psql/wire/copy.js +0 -108
  129. package/dist/psql/wire/notify.js +0 -59
  130. package/dist/psql/wire/pipeline.js +0 -521
  131. package/dist/psql/wire/protocol.js +0 -466
  132. package/dist/psql/wire/sasl.js +0 -294
  133. package/dist/psql/wire/tls.js +0 -602
  134. package/dist/storage_api.js +0 -147
  135. package/dist/test_utils/fixtures.js +0 -121
  136. package/dist/test_utils/oauth_server.js +0 -9
  137. package/dist/types.js +0 -1
  138. package/dist/utils/ai_gateway_notice.js +0 -180
  139. package/dist/utils/api_enums.js +0 -33
  140. package/dist/utils/auth.js +0 -5
  141. package/dist/utils/branch_notice.js +0 -22
  142. package/dist/utils/branch_picker.js +0 -103
  143. package/dist/utils/compute_units.js +0 -28
  144. package/dist/utils/config_diff.js +0 -185
  145. package/dist/utils/enrichers.js +0 -161
  146. package/dist/utils/esbuild.js +0 -158
  147. package/dist/utils/formats.js +0 -18
  148. package/dist/utils/git_diff.js +0 -90
  149. package/dist/utils/inspect_db.js +0 -153
  150. package/dist/utils/inspect_queries.js +0 -372
  151. package/dist/utils/middlewares.js +0 -20
  152. package/dist/utils/openapi.js +0 -114
  153. package/dist/utils/package_manager.js +0 -68
  154. package/dist/utils/point_in_time.js +0 -56
  155. package/dist/utils/psql.js +0 -120
  156. package/dist/utils/string.js +0 -5
  157. package/dist/utils/ui.js +0 -59
  158. package/dist/utils/zip.js +0 -4
  159. 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,60 +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
- .logo svg path {
35
- fill: #37c38f;
36
- }
37
- @media (prefers-color-scheme: dark) {
38
- .logo svg path {
39
- fill: #34d59a;
40
- }
41
- }
42
- </style>
43
- </head>
44
- <body>
45
- <div class="logo">
46
- <svg
47
- viewBox="0 0 64 64"
48
- fill="none"
49
- xmlns="http://www.w3.org/2000/svg"
50
- aria-hidden="true"
51
- >
52
- <path
53
- d="M63 0.0177909V63.5526L38.4178 42.2501V63.5526H0V0L63 0.0177909ZM7.72251 55.8389H30.6953V25.3238L55.2779 47.0476V7.72922L7.72251 7.71559V55.8389Z"
54
- />
55
- </svg>
56
- </div>
57
- <h1>Thank you for using Neon</h1>
58
- <p>You may close this page now</p>
59
- </body>
60
- </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");