neon 2.47.0 → 3.1.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/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
package/dist/_shared/profiles.js
CHANGED
|
@@ -1,276 +1,226 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
|
|
5
|
-
* keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
|
|
6
|
-
* "active profile" state to fall out of sync, and no migration.
|
|
7
|
-
*
|
|
8
|
-
* ```
|
|
9
|
-
* ~/.config/neon/
|
|
10
|
-
* ├── credentials.json # this IS the DEFAULT profile, not a copy of it
|
|
11
|
-
* ├── credentials.work.json # created by `neon auth --profile work`
|
|
12
|
-
* └── profiles.json # created only once a second profile exists
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* `profiles.json` maps a name to a path, and the path may point anywhere — which is what
|
|
16
|
-
* makes adopting an existing directory a one-line edit rather than an import command:
|
|
17
|
-
*
|
|
18
|
-
* ```json
|
|
19
|
-
* {
|
|
20
|
-
* "version": 1,
|
|
21
|
-
* "profiles": {
|
|
22
|
-
* "DEFAULT": { "credentials": "credentials.json" },
|
|
23
|
-
* "work": {
|
|
24
|
-
* "credentials": "../neonctl-databricks/credentials.json",
|
|
25
|
-
* "label": "someone@example.com"
|
|
26
|
-
* }
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* ## Selection
|
|
32
|
-
*
|
|
33
|
-
* `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
|
|
34
|
-
* `profile use` command, so nothing persists that could disagree with what you typed.
|
|
35
|
-
*
|
|
36
|
-
* ## Compatibility
|
|
37
|
-
*
|
|
38
|
-
* An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
|
|
39
|
-
* resolves to `credentials.json` in the config directory (including an existing one in the
|
|
40
|
-
* legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
|
|
41
|
-
* profile is, and nothing is ever moved.
|
|
42
|
-
*/
|
|
1
|
+
import { writeSecretFile } from "./secure_file.js";
|
|
2
|
+
import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
|
|
43
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
44
4
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
5
|
+
//#region src/_shared/profiles.ts
|
|
6
|
+
/**
|
|
7
|
+
* # Profiles — several Neon accounts in one config directory
|
|
8
|
+
*
|
|
9
|
+
* A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
|
|
10
|
+
* keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
|
|
11
|
+
* "active profile" state to fall out of sync, and no migration.
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* ~/.config/neon/
|
|
15
|
+
* ├── credentials.json # this IS the DEFAULT profile, not a copy of it
|
|
16
|
+
* ├── credentials.work.json # created by `neon auth --profile work`
|
|
17
|
+
* └── profiles.json # created only once a second profile exists
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `profiles.json` maps a name to a path, and the path may point anywhere — which is what
|
|
21
|
+
* makes adopting an existing directory a one-line edit rather than an import command:
|
|
22
|
+
*
|
|
23
|
+
* ```json
|
|
24
|
+
* {
|
|
25
|
+
* "version": 1,
|
|
26
|
+
* "profiles": {
|
|
27
|
+
* "DEFAULT": { "credentials": "credentials.json" },
|
|
28
|
+
* "work": {
|
|
29
|
+
* "credentials": "../neonctl-databricks/credentials.json",
|
|
30
|
+
* "label": "someone@example.com"
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ## Selection
|
|
37
|
+
*
|
|
38
|
+
* `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
|
|
39
|
+
* `profile use` command, so nothing persists that could disagree with what you typed.
|
|
40
|
+
*
|
|
41
|
+
* ## Compatibility
|
|
42
|
+
*
|
|
43
|
+
* An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
|
|
44
|
+
* resolves to `credentials.json` in the config directory (including an existing one in the
|
|
45
|
+
* legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
|
|
46
|
+
* profile is, and nothing is ever moved.
|
|
47
|
+
*/
|
|
48
|
+
const PROFILES_FILE = "profiles.json";
|
|
48
49
|
/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
|
|
49
|
-
|
|
50
|
+
const DEFAULT_PROFILE = "DEFAULT";
|
|
50
51
|
/** Profile names become part of a filename, so keep them boring. */
|
|
51
52
|
const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
52
53
|
/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new Error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`);
|
|
57
|
-
}
|
|
54
|
+
const selectProfileName = (flag, env = process.env) => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? "DEFAULT";
|
|
55
|
+
const assertValidProfileName = (name) => {
|
|
56
|
+
if (!NAME_PATTERN.test(name)) throw new Error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`);
|
|
58
57
|
};
|
|
59
58
|
/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
|
|
60
|
-
|
|
59
|
+
const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
61
60
|
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!NAME_PATTERN.test(name))
|
|
104
|
-
return broken(`"${name}" is not a valid profile name`);
|
|
105
|
-
if (entry === null ||
|
|
106
|
-
typeof entry !== "object" ||
|
|
107
|
-
typeof entry.credentials !== "string" ||
|
|
108
|
-
entry.credentials.trim() === "") {
|
|
109
|
-
return broken(`profile "${name}" has no \`credentials\` path`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { kind: "ok", file: { version: 1, profiles } };
|
|
61
|
+
* Read and classify `profiles.json` without deciding what to do about it.
|
|
62
|
+
*
|
|
63
|
+
* Entry keys and shapes are validated here rather than at each use. A key is a profile name,
|
|
64
|
+
* and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
|
|
65
|
+
* it would travel into error messages as a recovery command nobody can run, and into a
|
|
66
|
+
* `credentials.<name>.json` filename.
|
|
67
|
+
*/
|
|
68
|
+
const inspectProfiles = (dir) => {
|
|
69
|
+
const path = profilesFilePath(dir);
|
|
70
|
+
if (!existsSync(path)) return { kind: "absent" };
|
|
71
|
+
const broken = (why) => ({
|
|
72
|
+
kind: "unusable",
|
|
73
|
+
reason: `${path} could not be read as a profiles file: ${why}`
|
|
74
|
+
});
|
|
75
|
+
let contents;
|
|
76
|
+
try {
|
|
77
|
+
contents = readFileSync(path, "utf8");
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const code = err.code;
|
|
80
|
+
return broken(code ? `reading it failed with ${code}` : "reading it failed");
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(contents);
|
|
85
|
+
} catch {
|
|
86
|
+
return broken("it is not valid JSON");
|
|
87
|
+
}
|
|
88
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return broken("it does not contain an object");
|
|
89
|
+
const profiles = parsed.profiles;
|
|
90
|
+
if (profiles === null || typeof profiles !== "object" || Array.isArray(profiles)) return broken("it has no `profiles` object");
|
|
91
|
+
for (const [name, entry] of Object.entries(profiles)) {
|
|
92
|
+
if (!NAME_PATTERN.test(name)) return broken(`"${name}" is not a valid profile name`);
|
|
93
|
+
if (entry === null || typeof entry !== "object" || typeof entry.credentials !== "string" || entry.credentials.trim() === "") return broken(`profile "${name}" has no \`credentials\` path`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
kind: "ok",
|
|
97
|
+
file: {
|
|
98
|
+
version: 1,
|
|
99
|
+
profiles
|
|
100
|
+
}
|
|
101
|
+
};
|
|
113
102
|
};
|
|
114
103
|
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return read.file;
|
|
128
|
-
if (read.kind === "unusable")
|
|
129
|
-
onWarn(read.reason);
|
|
130
|
-
return null;
|
|
104
|
+
* Read `profiles.json`, or `null` when there is nothing usable there.
|
|
105
|
+
*
|
|
106
|
+
* A malformed file is reported through `onWarn` and treated as absent, because for a *read* the
|
|
107
|
+
* worst case is a named profile turning up missing, which is recoverable — whereas throwing
|
|
108
|
+
* would lock the user out of `neon auth` itself. Writing is the opposite: see
|
|
109
|
+
* {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.
|
|
110
|
+
*/
|
|
111
|
+
const readProfiles = (dir, onWarn = () => {}) => {
|
|
112
|
+
const read = inspectProfiles(dir);
|
|
113
|
+
if (read.kind === "ok") return read.file;
|
|
114
|
+
if (read.kind === "unusable") onWarn(read.reason);
|
|
115
|
+
return null;
|
|
131
116
|
};
|
|
132
117
|
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (read.kind === "unusable") {
|
|
150
|
-
throw new Error(`${read.reason}. Fix or delete the file before working with profile "${name}" — it is the only record of where each account's credentials live.`);
|
|
151
|
-
}
|
|
118
|
+
* Refuse to act on a named profile when the file that defines it cannot be read.
|
|
119
|
+
*
|
|
120
|
+
* Call this **before** anything that writes a credential, opens a browser, or spends an API
|
|
121
|
+
* call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already
|
|
122
|
+
* overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth
|
|
123
|
+
* --profile` has already signed in over it — a refusal that arrives after the destruction it
|
|
124
|
+
* exists to prevent. The path resolution itself is the unsound part, since with the metadata
|
|
125
|
+
* unreadable the conventional filename is a guess about which account that file belongs to.
|
|
126
|
+
*
|
|
127
|
+
* `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so
|
|
128
|
+
* signing in normally must keep working while a broken `profiles.json` is repaired.
|
|
129
|
+
*/
|
|
130
|
+
const assertProfilesUsable = (dir, name) => {
|
|
131
|
+
if (name === "DEFAULT") return;
|
|
132
|
+
const read = inspectProfiles(dir);
|
|
133
|
+
if (read.kind === "unusable") throw new Error(`${read.reason}. Fix or delete the file before working with profile "${name}" — it is the only record of where each account's credentials live.`);
|
|
152
134
|
};
|
|
153
135
|
/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// doesn't mention it — that is the pre-profiles behaviour, unchanged.
|
|
174
|
-
if (name === DEFAULT_PROFILE) {
|
|
175
|
-
return {
|
|
176
|
-
name,
|
|
177
|
-
credentialsPath: credentialsPath(dir),
|
|
178
|
-
declared: false,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const known = file
|
|
182
|
-
? Object.keys(file.profiles).join(", ")
|
|
183
|
-
: DEFAULT_PROFILE;
|
|
184
|
-
throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
|
|
136
|
+
const resolveProfile = (dir, name) => {
|
|
137
|
+
const read = inspectProfiles(dir);
|
|
138
|
+
if (read.kind === "unusable" && name !== "DEFAULT") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
|
|
139
|
+
const file = read.kind === "ok" ? read.file : null;
|
|
140
|
+
const entry = file?.profiles[name];
|
|
141
|
+
if (entry) return {
|
|
142
|
+
name,
|
|
143
|
+
credentialsPath: resolveEntryPath(dir, entry.credentials),
|
|
144
|
+
...entry.label ? { label: entry.label } : {},
|
|
145
|
+
...entry.userId ? { userId: entry.userId } : {},
|
|
146
|
+
declared: true
|
|
147
|
+
};
|
|
148
|
+
if (name === "DEFAULT") return {
|
|
149
|
+
name,
|
|
150
|
+
credentialsPath: credentialsPath(dir),
|
|
151
|
+
declared: false
|
|
152
|
+
};
|
|
153
|
+
const known = file ? Object.keys(file.profiles).join(", ") : DEFAULT_PROFILE;
|
|
154
|
+
throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
|
|
185
155
|
};
|
|
186
156
|
/** Default location for a new named profile's credentials file. */
|
|
187
|
-
|
|
157
|
+
const newProfileCredentialsPath = (dir, name) => resolve(dir, `credentials.${name}.json`);
|
|
188
158
|
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
profiles: {
|
|
213
|
-
[DEFAULT_PROFILE]: {
|
|
214
|
-
credentials: relativeToProfiles(path, credentialsPath(dir)),
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
file.profiles[name] = {
|
|
219
|
-
credentials: relativeToProfiles(path, entry.credentials),
|
|
220
|
-
...(entry.label ? { label: entry.label } : {}),
|
|
221
|
-
...(entry.userId ? { userId: entry.userId } : {}),
|
|
222
|
-
};
|
|
223
|
-
writeProfiles(path, file);
|
|
159
|
+
* Record a profile, creating `profiles.json` if this is the first named one.
|
|
160
|
+
*
|
|
161
|
+
* When the file is created, `DEFAULT` is written explicitly and pointed at wherever
|
|
162
|
+
* `credentials.json` actually is. That matters for an install predating the directory
|
|
163
|
+
* rename: `profiles.json` is created in `neon/` while the credentials are still in
|
|
164
|
+
* `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a
|
|
165
|
+
* relative name that would resolve to a file that isn't there.
|
|
166
|
+
*/
|
|
167
|
+
const upsertProfile = (dir, name, entry) => {
|
|
168
|
+
assertValidProfileName(name);
|
|
169
|
+
const path = profilesFilePath(dir);
|
|
170
|
+
const read = inspectProfiles(dir);
|
|
171
|
+
if (read.kind === "unusable") throw new Error(`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`);
|
|
172
|
+
const file = read.kind === "ok" ? read.file : {
|
|
173
|
+
version: 1,
|
|
174
|
+
profiles: { [DEFAULT_PROFILE]: { credentials: relativeToProfiles(path, credentialsPath(dir)) } }
|
|
175
|
+
};
|
|
176
|
+
file.profiles[name] = {
|
|
177
|
+
credentials: relativeToProfiles(path, entry.credentials),
|
|
178
|
+
...entry.label ? { label: entry.label } : {},
|
|
179
|
+
...entry.userId ? { userId: entry.userId } : {}
|
|
180
|
+
};
|
|
181
|
+
writeProfiles(path, file);
|
|
224
182
|
};
|
|
225
183
|
/** Remove an entry. Returns false when it wasn't there. */
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return true;
|
|
184
|
+
const removeProfileEntry = (dir, name) => {
|
|
185
|
+
const path = profilesFilePath(dir);
|
|
186
|
+
const file = readProfiles(dir);
|
|
187
|
+
if (!file?.profiles[name]) return false;
|
|
188
|
+
delete file.profiles[name];
|
|
189
|
+
writeProfiles(path, file);
|
|
190
|
+
return true;
|
|
234
191
|
};
|
|
235
192
|
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
(names.length === 1 && names[0] === DEFAULT_PROFILE));
|
|
193
|
+
* True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors
|
|
194
|
+
* lazy creation: a single-account install has no profiles file, before or after.
|
|
195
|
+
*/
|
|
196
|
+
const onlyDefaultRemains = (file) => {
|
|
197
|
+
const names = Object.keys(file.profiles);
|
|
198
|
+
return names.length === 0 || names.length === 1 && names[0] === "DEFAULT";
|
|
243
199
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const file = read.kind === "ok" ? read.file : null;
|
|
253
|
-
if (!file)
|
|
254
|
-
return [resolveProfile(dir, DEFAULT_PROFILE)];
|
|
255
|
-
const names = Object.keys(file.profiles);
|
|
256
|
-
if (!names.includes(DEFAULT_PROFILE))
|
|
257
|
-
names.unshift(DEFAULT_PROFILE);
|
|
258
|
-
return names.map((name) => resolveProfile(dir, name));
|
|
200
|
+
const listProfiles = (dir) => {
|
|
201
|
+
const read = inspectProfiles(dir);
|
|
202
|
+
if (read.kind === "unusable") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
|
|
203
|
+
const file = read.kind === "ok" ? read.file : null;
|
|
204
|
+
if (!file) return [resolveProfile(dir, DEFAULT_PROFILE)];
|
|
205
|
+
const names = Object.keys(file.profiles);
|
|
206
|
+
if (!names.includes("DEFAULT")) names.unshift(DEFAULT_PROFILE);
|
|
207
|
+
return names.map((name) => resolveProfile(dir, name));
|
|
259
208
|
};
|
|
260
209
|
const writeProfiles = (path, file) => {
|
|
261
|
-
|
|
210
|
+
writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
262
211
|
};
|
|
263
212
|
const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
|
|
264
213
|
/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
|
|
265
214
|
const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
|
|
266
215
|
/** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */
|
|
267
216
|
const relativeToProfiles = (profilesPath, target) => {
|
|
268
|
-
|
|
269
|
-
|
|
217
|
+
const rel = relative(resolve(profilesPath, ".."), target);
|
|
218
|
+
return rel && !isAbsolute(rel) ? rel : target;
|
|
270
219
|
};
|
|
271
220
|
function nonEmpty(value) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return trimmed === "" ? undefined : trimmed;
|
|
221
|
+
if (typeof value !== "string") return void 0;
|
|
222
|
+
const trimmed = value.trim();
|
|
223
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
276
224
|
}
|
|
225
|
+
//#endregion
|
|
226
|
+
export { DEFAULT_PROFILE, PROFILES_FILE, assertProfilesUsable, assertValidProfileName, inspectProfiles, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, removeProfileEntry, resolveProfile, selectProfileName, upsertProfile };
|
|
@@ -1,43 +1,41 @@
|
|
|
1
1
|
import { renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
|
+
//#region src/_shared/secure_file.ts
|
|
3
4
|
/** Owner read/write. A credential needs those two and nothing else. */
|
|
4
|
-
|
|
5
|
+
const SECRET_FILE_MODE = 384;
|
|
5
6
|
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
catch {
|
|
39
|
-
// The temp file was never created, or is already gone. Report the original error.
|
|
40
|
-
}
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
7
|
+
* Write a secret to disk owner-only, by creating a temporary file in the same directory and
|
|
8
|
+
* renaming it over the target.
|
|
9
|
+
*
|
|
10
|
+
* The rename is what makes this correct rather than merely tidy. `writeFileSync`'s `mode`
|
|
11
|
+
* applies only when it *creates* the file, so writing over an existing credentials file
|
|
12
|
+
* leaves whatever permissions it already had — a file created `0700` by an older release
|
|
13
|
+
* stays `0700` forever, and one created before a umask change stays world-readable. Renaming
|
|
14
|
+
* a fresh inode into place means every write lands at {@link SECRET_FILE_MODE}, so the
|
|
15
|
+
* permissions repair themselves instead of being inherited.
|
|
16
|
+
*
|
|
17
|
+
* It also closes the window where a reader could see the file at default permissions: the
|
|
18
|
+
* temporary file is created `0600` *before* it holds the secret's final name, and `rename`
|
|
19
|
+
* is atomic within a directory, so there is no moment at which the target is readable by
|
|
20
|
+
* anyone else and no moment at which it is half-written.
|
|
21
|
+
*
|
|
22
|
+
* The temporary name carries the pid so two processes writing at once cannot collide on it.
|
|
23
|
+
*/
|
|
24
|
+
const writeSecretFile = (path, contents) => {
|
|
25
|
+
const directory = dirname(path);
|
|
26
|
+
const temporary = join(directory, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(temporary, contents, {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
mode: 384
|
|
31
|
+
});
|
|
32
|
+
renameSync(temporary, path);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
try {
|
|
35
|
+
unlinkSync(temporary);
|
|
36
|
+
} catch {}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
43
39
|
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { SECRET_FILE_MODE, writeSecretFile };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll };
|