neon 2.1.1 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +178 -0
- package/README.md +601 -0
- package/dist/analytics.js +156 -0
- package/dist/api.js +665 -0
- package/dist/auth.js +127 -0
- package/dist/callback.html +51 -0
- package/dist/cli.js +9 -0
- package/dist/commands/auth.js +214 -0
- package/dist/commands/bootstrap.js +481 -0
- package/dist/commands/branches.js +481 -0
- package/dist/commands/bucket.js +543 -0
- package/dist/commands/checkout.js +289 -0
- package/dist/commands/config.js +544 -0
- package/dist/commands/connection_string.js +172 -0
- package/dist/commands/data_api.js +285 -0
- package/dist/commands/databases.js +82 -0
- package/dist/commands/deploy.js +26 -0
- package/dist/commands/dev.js +698 -0
- package/dist/commands/env.js +166 -0
- package/dist/commands/functions.js +373 -0
- package/dist/commands/index.js +54 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/ip_allow.js +137 -0
- package/dist/commands/link.js +1121 -0
- package/dist/commands/neon_auth.js +1028 -0
- package/dist/commands/operations.js +28 -0
- package/dist/commands/orgs.js +24 -0
- package/dist/commands/projects.js +372 -0
- package/dist/commands/psql.js +62 -0
- package/dist/commands/roles.js +65 -0
- package/dist/commands/schema_diff.js +151 -0
- package/dist/commands/set_context.js +29 -0
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +15 -0
- package/dist/commands/vpc_endpoints.js +134 -0
- package/dist/config.js +11 -0
- package/dist/config_format.js +72 -0
- package/dist/context.js +177 -0
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +240 -0
- package/dist/dev/functions.js +70 -0
- package/dist/dev/inputs.js +63 -0
- package/dist/dev/runtime.js +146 -0
- package/dist/env.js +36 -0
- package/dist/env_file.js +159 -0
- package/dist/errors.js +80 -0
- package/dist/functions_api.js +44 -0
- package/dist/help.js +146 -0
- package/dist/index.js +234 -0
- package/dist/log.js +18 -0
- package/dist/parameters.gen.js +480 -0
- package/dist/pkg.js +25 -0
- package/dist/psql/cli.js +53 -0
- package/dist/psql/command/cmd_cond.js +437 -0
- package/dist/psql/command/cmd_connect.js +820 -0
- package/dist/psql/command/cmd_copy.js +1035 -0
- package/dist/psql/command/cmd_describe.js +1815 -0
- package/dist/psql/command/cmd_format.js +922 -0
- package/dist/psql/command/cmd_io.js +2193 -0
- package/dist/psql/command/cmd_lo.js +393 -0
- package/dist/psql/command/cmd_meta.js +970 -0
- package/dist/psql/command/cmd_misc.js +187 -0
- package/dist/psql/command/cmd_pipeline.js +1148 -0
- package/dist/psql/command/cmd_restrict.js +171 -0
- package/dist/psql/command/cmd_show.js +766 -0
- package/dist/psql/command/dispatch.js +343 -0
- package/dist/psql/command/inputQueue.js +42 -0
- package/dist/psql/command/shared.js +71 -0
- package/dist/psql/complete/filenames.js +139 -0
- package/dist/psql/complete/index.js +104 -0
- package/dist/psql/complete/matcher.js +315 -0
- package/dist/psql/complete/psqlVars.js +247 -0
- package/dist/psql/complete/queries.js +493 -0
- package/dist/psql/complete/rules.js +2424 -0
- package/dist/psql/core/common.js +1253 -0
- package/dist/psql/core/help.js +576 -0
- package/dist/psql/core/mainloop.js +1360 -0
- package/dist/psql/core/prompt.js +440 -0
- package/dist/psql/core/settings.js +684 -0
- package/dist/psql/core/sqlHelp.js +1066 -0
- package/dist/psql/core/startup.js +846 -0
- package/dist/psql/core/syncVars.js +116 -0
- package/dist/psql/core/variables.js +287 -0
- package/dist/psql/describe/formatters.js +1290 -0
- package/dist/psql/describe/processNamePattern.js +270 -0
- package/dist/psql/describe/queries.js +2378 -0
- package/dist/psql/describe/versionGate.js +43 -0
- package/dist/psql/index.js +2030 -0
- package/dist/psql/io/history.js +299 -0
- package/dist/psql/io/input.js +120 -0
- package/dist/psql/io/lineEditor/buffer.js +325 -0
- package/dist/psql/io/lineEditor/complete.js +227 -0
- package/dist/psql/io/lineEditor/filename.js +159 -0
- package/dist/psql/io/lineEditor/index.js +894 -0
- package/dist/psql/io/lineEditor/keymap.js +745 -0
- package/dist/psql/io/lineEditor/vt100.js +363 -0
- package/dist/psql/io/pgpass.js +202 -0
- package/dist/psql/io/pgservice.js +194 -0
- package/dist/psql/io/psqlrc.js +422 -0
- package/dist/psql/print/aligned.js +1765 -0
- package/dist/psql/print/asciidoc.js +248 -0
- package/dist/psql/print/crosstab.js +463 -0
- package/dist/psql/print/csv.js +95 -0
- package/dist/psql/print/html.js +258 -0
- package/dist/psql/print/json.js +96 -0
- package/dist/psql/print/latex.js +396 -0
- package/dist/psql/print/pager.js +267 -0
- package/dist/psql/print/troff.js +258 -0
- package/dist/psql/print/unaligned.js +119 -0
- package/dist/psql/print/units.js +135 -0
- package/dist/psql/scanner/slash.js +515 -0
- package/dist/psql/scanner/sql.js +914 -0
- package/dist/psql/scanner/stringutils.js +394 -0
- package/dist/psql/types/backslash.js +1 -0
- package/dist/psql/types/connection.js +1 -0
- package/dist/psql/types/index.js +7 -0
- package/dist/psql/types/printer.js +1 -0
- package/dist/psql/types/repl.js +1 -0
- package/dist/psql/types/scanner.js +24 -0
- package/dist/psql/types/settings.js +1 -0
- package/dist/psql/types/variables.js +1 -0
- package/dist/psql/wire/connection.js +2858 -0
- package/dist/psql/wire/copy.js +108 -0
- package/dist/psql/wire/notify.js +59 -0
- package/dist/psql/wire/pipeline.js +521 -0
- package/dist/psql/wire/protocol.js +466 -0
- package/dist/psql/wire/sasl.js +296 -0
- package/dist/psql/wire/tls.js +602 -0
- package/dist/storage_api.js +147 -0
- package/dist/test_utils/fixtures.js +122 -0
- package/dist/test_utils/oauth_server.js +9 -0
- package/dist/types.js +1 -0
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/auth.js +5 -0
- package/dist/utils/branch_notice.js +22 -0
- package/dist/utils/branch_picker.js +103 -0
- package/dist/utils/compute_units.js +28 -0
- package/dist/utils/enrichers.js +161 -0
- package/dist/utils/esbuild.js +158 -0
- package/dist/utils/formats.js +18 -0
- package/dist/utils/middlewares.js +20 -0
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +56 -0
- package/dist/utils/psql.js +120 -0
- package/dist/utils/string.js +5 -0
- package/dist/utils/ui.js +59 -0
- package/dist/utils/zip.js +4 -0
- package/dist/writer.js +97 -0
- package/package.json +117 -14
- package/.jshintrc +0 -3
- package/CHANGELOG.markdown +0 -22
- package/LICENSE +0 -21
- package/README.markdown +0 -76
- package/bower.json +0 -23
- package/license.txt +0 -9
- package/neon.js +0 -234
- package/stdlib/bubbling_support.js +0 -32
- package/stdlib/custom_event.js +0 -54
- package/stdlib/custom_event_support.js +0 -195
- package/stdlib/index.js +0 -7
- package/stdlib/node_support.js +0 -123
- package/stdlib/widget.js +0 -340
- package/test/neon_browser.html +0 -11
- package/test/neon_stdlib_browser.html +0 -15
- package/test/neon_stdlib_test.js +0 -84
- package/test/neon_test.js +0 -64
package/dist/auth.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
? // eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
37
|
+
[client.allowInsecureRequests]
|
|
38
|
+
: undefined,
|
|
39
|
+
});
|
|
40
|
+
return await client.refreshTokenGrant(configuration, tokenSet.refresh_token);
|
|
41
|
+
};
|
|
42
|
+
export const auth = async ({ oauthHost, clientId, allowUnsafeTls, }) => {
|
|
43
|
+
log.debug("Discovering oauth server");
|
|
44
|
+
const configuration = await client.discovery(new URL(oauthHost), clientId, { token_endpoint_auth_method: "none" }, client.None(), {
|
|
45
|
+
timeout: SERVER_TIMEOUT,
|
|
46
|
+
execute: allowUnsafeTls
|
|
47
|
+
? // eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
48
|
+
[client.allowInsecureRequests]
|
|
49
|
+
: undefined,
|
|
50
|
+
});
|
|
51
|
+
//
|
|
52
|
+
// Start HTTP server and wait till /callback is hit
|
|
53
|
+
//
|
|
54
|
+
log.debug("Starting HTTP Server for callback");
|
|
55
|
+
const server = createServer();
|
|
56
|
+
server.listen(0, "127.0.0.1", function () {
|
|
57
|
+
log.debug(`Listening on port ${this.address().port}`);
|
|
58
|
+
});
|
|
59
|
+
await new Promise((resolve) => server.once("listening", resolve));
|
|
60
|
+
const listen_port = server.address().port;
|
|
61
|
+
// https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
|
|
62
|
+
const state = client.randomState();
|
|
63
|
+
// we store the code_verifier in memory
|
|
64
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
65
|
+
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
reject(new Error(`Authentication timed out after ${AUTH_TIMEOUT_SECONDS} seconds`));
|
|
69
|
+
}, AUTH_TIMEOUT_SECONDS * 1000);
|
|
70
|
+
const onRequest = async (request, response) => {
|
|
71
|
+
//
|
|
72
|
+
// Wait for callback and follow oauth flow.
|
|
73
|
+
//
|
|
74
|
+
if (!request.url?.startsWith("/callback")) {
|
|
75
|
+
response.writeHead(404);
|
|
76
|
+
response.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// process the CORS preflight OPTIONS request
|
|
80
|
+
if (request.method === "OPTIONS") {
|
|
81
|
+
response.writeHead(200, {
|
|
82
|
+
"Access-Control-Allow-Origin": "*",
|
|
83
|
+
"Access-Control-Allow-Methods": "GET, POST",
|
|
84
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
85
|
+
});
|
|
86
|
+
response.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
log.debug(`Callback received: ${request.url}`);
|
|
90
|
+
const tokenSet = await client.authorizationCodeGrant(configuration, new URL(request.url, `http://127.0.0.1:${listen_port}`), {
|
|
91
|
+
pkceCodeVerifier: codeVerifier,
|
|
92
|
+
expectedState: state,
|
|
93
|
+
});
|
|
94
|
+
response.writeHead(200, { "Content-Type": "text/html" });
|
|
95
|
+
createReadStream(join(fileURLToPath(new URL(".", import.meta.url)), "./callback.html")).pipe(response);
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
const exp = new Date();
|
|
98
|
+
exp.setSeconds(exp.getSeconds() + (tokenSet.expires_in ?? 0));
|
|
99
|
+
resolve(extendTokenSet(tokenSet));
|
|
100
|
+
server.close();
|
|
101
|
+
};
|
|
102
|
+
server.on("request", (req, res) => {
|
|
103
|
+
void onRequest(req, res);
|
|
104
|
+
});
|
|
105
|
+
//
|
|
106
|
+
// Open browser to let user authenticate
|
|
107
|
+
//
|
|
108
|
+
const scopes = clientId == defaultClientID
|
|
109
|
+
? NEONCTL_SCOPES
|
|
110
|
+
: ALWAYS_PRESENT_SCOPES;
|
|
111
|
+
const authUrl = client.buildAuthorizationUrl(configuration, {
|
|
112
|
+
scope: scopes.join(" "),
|
|
113
|
+
state,
|
|
114
|
+
code_challenge: codeChallenge,
|
|
115
|
+
code_challenge_method: "S256",
|
|
116
|
+
redirect_uri: REDIRECT_URI(listen_port),
|
|
117
|
+
});
|
|
118
|
+
log.info("Awaiting authentication in web browser.");
|
|
119
|
+
log.info(`Auth Url: ${authUrl}`);
|
|
120
|
+
open(authUrl.href).catch((err) => {
|
|
121
|
+
const msg = `Failed to open web browser. Please copy & paste auth url to authenticate in browser.`;
|
|
122
|
+
const typedErr = err && err instanceof Error ? err : undefined;
|
|
123
|
+
sendError(typedErr || new Error(msg), matchErrorCode(msg));
|
|
124
|
+
log.error(msg);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
// Making the linter happy by explicitly confirming we don't care about @typescript-eslint/no-misused-spread
|
|
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");
|