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.
- package/README.md +15 -700
- package/bin/cli.js +3 -0
- package/package.json +8 -75
- package/dist/analytics.js +0 -168
- package/dist/api.js +0 -720
- package/dist/auth.js +0 -125
- package/dist/callback.html +0 -60
- package/dist/cli.js +0 -9
- package/dist/commands/api.js +0 -278
- package/dist/commands/auth.js +0 -214
- package/dist/commands/bootstrap.js +0 -481
- package/dist/commands/branches.js +0 -488
- package/dist/commands/bucket.js +0 -549
- package/dist/commands/checkout.js +0 -321
- package/dist/commands/config.js +0 -626
- package/dist/commands/connection_string.js +0 -172
- package/dist/commands/data_api.js +0 -284
- package/dist/commands/databases.js +0 -82
- package/dist/commands/deploy.js +0 -26
- package/dist/commands/dev.js +0 -698
- package/dist/commands/diff.js +0 -222
- package/dist/commands/env.js +0 -189
- package/dist/commands/functions.js +0 -373
- package/dist/commands/index.js +0 -62
- package/dist/commands/init.js +0 -73
- package/dist/commands/inspect.js +0 -65
- package/dist/commands/ip_allow.js +0 -137
- package/dist/commands/link.js +0 -1121
- package/dist/commands/neon_auth.js +0 -1028
- package/dist/commands/operations.js +0 -28
- package/dist/commands/orgs.js +0 -24
- package/dist/commands/projects.js +0 -413
- package/dist/commands/psql.js +0 -62
- package/dist/commands/roles.js +0 -65
- package/dist/commands/schema_diff.js +0 -151
- package/dist/commands/set_context.js +0 -29
- package/dist/commands/snapshots.js +0 -455
- package/dist/commands/status.js +0 -40
- package/dist/commands/user.js +0 -15
- package/dist/commands/vpc_endpoints.js +0 -134
- package/dist/config.js +0 -11
- package/dist/config_format.js +0 -72
- package/dist/context.js +0 -231
- package/dist/current_branch_fast_path.js +0 -55
- package/dist/dev/env.js +0 -244
- package/dist/dev/functions.js +0 -70
- package/dist/dev/inputs.js +0 -63
- package/dist/dev/runtime.js +0 -146
- package/dist/env.js +0 -36
- package/dist/env_file.js +0 -159
- package/dist/errors.js +0 -80
- package/dist/functions_api.js +0 -48
- package/dist/help.js +0 -146
- package/dist/index.js +0 -240
- package/dist/log.js +0 -18
- package/dist/parameters.gen.js +0 -480
- package/dist/pkg.js +0 -25
- package/dist/psql/cli.js +0 -53
- package/dist/psql/command/cmd_cond.js +0 -437
- package/dist/psql/command/cmd_connect.js +0 -820
- package/dist/psql/command/cmd_copy.js +0 -1035
- package/dist/psql/command/cmd_describe.js +0 -1815
- package/dist/psql/command/cmd_format.js +0 -948
- package/dist/psql/command/cmd_io.js +0 -2193
- package/dist/psql/command/cmd_lo.js +0 -393
- package/dist/psql/command/cmd_meta.js +0 -969
- package/dist/psql/command/cmd_misc.js +0 -187
- package/dist/psql/command/cmd_pipeline.js +0 -1148
- package/dist/psql/command/cmd_restrict.js +0 -171
- package/dist/psql/command/cmd_show.js +0 -766
- package/dist/psql/command/dispatch.js +0 -343
- package/dist/psql/command/inputQueue.js +0 -42
- package/dist/psql/command/shared.js +0 -71
- package/dist/psql/complete/filenames.js +0 -139
- package/dist/psql/complete/index.js +0 -104
- package/dist/psql/complete/matcher.js +0 -315
- package/dist/psql/complete/psqlVars.js +0 -249
- package/dist/psql/complete/queries.js +0 -493
- package/dist/psql/complete/rules.js +0 -2424
- package/dist/psql/core/common.js +0 -1253
- package/dist/psql/core/help.js +0 -576
- package/dist/psql/core/mainloop.js +0 -1360
- package/dist/psql/core/prompt.js +0 -439
- package/dist/psql/core/settings.js +0 -686
- package/dist/psql/core/sqlHelp.js +0 -1066
- package/dist/psql/core/startup.js +0 -846
- package/dist/psql/core/syncVars.js +0 -116
- package/dist/psql/core/variables.js +0 -287
- package/dist/psql/describe/formatters.js +0 -1330
- package/dist/psql/describe/processNamePattern.js +0 -270
- package/dist/psql/describe/queries.js +0 -2452
- package/dist/psql/describe/versionGate.js +0 -44
- package/dist/psql/index.js +0 -2030
- package/dist/psql/io/history.js +0 -299
- package/dist/psql/io/input.js +0 -120
- package/dist/psql/io/lineEditor/buffer.js +0 -325
- package/dist/psql/io/lineEditor/complete.js +0 -227
- package/dist/psql/io/lineEditor/filename.js +0 -159
- package/dist/psql/io/lineEditor/index.js +0 -893
- package/dist/psql/io/lineEditor/keymap.js +0 -745
- package/dist/psql/io/lineEditor/vt100.js +0 -363
- package/dist/psql/io/pgpass.js +0 -202
- package/dist/psql/io/pgservice.js +0 -194
- package/dist/psql/io/psqlrc.js +0 -422
- package/dist/psql/print/aligned.js +0 -1748
- package/dist/psql/print/asciidoc.js +0 -230
- package/dist/psql/print/crosstab.js +0 -463
- package/dist/psql/print/csv.js +0 -76
- package/dist/psql/print/html.js +0 -240
- package/dist/psql/print/json.js +0 -96
- package/dist/psql/print/latex.js +0 -379
- package/dist/psql/print/pager.js +0 -267
- package/dist/psql/print/troff.js +0 -240
- package/dist/psql/print/unaligned.js +0 -99
- package/dist/psql/print/units.js +0 -188
- package/dist/psql/scanner/slash.js +0 -515
- package/dist/psql/scanner/sql.js +0 -914
- package/dist/psql/scanner/stringutils.js +0 -394
- package/dist/psql/types/backslash.js +0 -1
- package/dist/psql/types/connection.js +0 -1
- package/dist/psql/types/index.js +0 -7
- package/dist/psql/types/printer.js +0 -1
- package/dist/psql/types/repl.js +0 -1
- package/dist/psql/types/scanner.js +0 -24
- package/dist/psql/types/settings.js +0 -1
- package/dist/psql/types/variables.js +0 -1
- package/dist/psql/wire/connection.js +0 -2857
- package/dist/psql/wire/copy.js +0 -108
- package/dist/psql/wire/notify.js +0 -59
- package/dist/psql/wire/pipeline.js +0 -521
- package/dist/psql/wire/protocol.js +0 -466
- package/dist/psql/wire/sasl.js +0 -294
- package/dist/psql/wire/tls.js +0 -602
- package/dist/storage_api.js +0 -147
- package/dist/test_utils/fixtures.js +0 -121
- package/dist/test_utils/oauth_server.js +0 -9
- package/dist/types.js +0 -1
- package/dist/utils/ai_gateway_notice.js +0 -180
- package/dist/utils/api_enums.js +0 -33
- package/dist/utils/auth.js +0 -5
- package/dist/utils/branch_notice.js +0 -22
- package/dist/utils/branch_picker.js +0 -103
- package/dist/utils/compute_units.js +0 -28
- package/dist/utils/config_diff.js +0 -185
- package/dist/utils/enrichers.js +0 -161
- package/dist/utils/esbuild.js +0 -158
- package/dist/utils/formats.js +0 -18
- package/dist/utils/git_diff.js +0 -90
- package/dist/utils/inspect_db.js +0 -153
- package/dist/utils/inspect_queries.js +0 -372
- package/dist/utils/middlewares.js +0 -20
- package/dist/utils/openapi.js +0 -114
- package/dist/utils/package_manager.js +0 -68
- package/dist/utils/point_in_time.js +0 -56
- package/dist/utils/psql.js +0 -120
- package/dist/utils/string.js +0 -5
- package/dist/utils/ui.js +0 -59
- package/dist/utils/zip.js +0 -4
- 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
|
-
};
|
package/dist/callback.html
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/api.js
DELETED
|
@@ -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
|
-
};
|
package/dist/commands/auth.js
DELETED
|
@@ -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");
|