@neondatabase/env 1.0.0 → 1.0.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 +13 -0
- package/dist/cli.js +971 -6
- package/dist/cli.js.map +1 -1
- package/dist/{_shared/env-core/env.js → env.js} +62 -41
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +528 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +197 -2
- package/dist/{lib/parse-env.js.map → index.js.map} +1 -1
- package/package.json +8 -6
- package/dist/_shared/auth_selection.d.ts +0 -95
- package/dist/_shared/auth_selection.d.ts.map +0 -1
- package/dist/_shared/auth_selection.js +0 -85
- package/dist/_shared/auth_selection.js.map +0 -1
- package/dist/_shared/credentials.d.ts +0 -186
- package/dist/_shared/credentials.d.ts.map +0 -1
- package/dist/_shared/credentials.js +0 -190
- package/dist/_shared/credentials.js.map +0 -1
- package/dist/_shared/env-core/env.d.ts +0 -424
- package/dist/_shared/env-core/env.d.ts.map +0 -1
- package/dist/_shared/env-core/env.js.map +0 -1
- package/dist/_shared/env-core/reuse-secrets.d.ts +0 -95
- package/dist/_shared/env-core/reuse-secrets.d.ts.map +0 -1
- package/dist/_shared/env-core/reuse-secrets.js +0 -181
- package/dist/_shared/env-core/reuse-secrets.js.map +0 -1
- package/dist/_shared/paths.d.ts +0 -116
- package/dist/_shared/paths.d.ts.map +0 -1
- package/dist/_shared/paths.js +0 -153
- package/dist/_shared/paths.js.map +0 -1
- package/dist/_shared/profiles.d.ts +0 -145
- package/dist/_shared/profiles.d.ts.map +0 -1
- package/dist/_shared/profiles.js +0 -228
- package/dist/_shared/profiles.js.map +0 -1
- package/dist/_shared/secure_file.d.ts +0 -25
- package/dist/_shared/secure_file.d.ts.map +0 -1
- package/dist/_shared/secure_file.js +0 -43
- package/dist/_shared/secure_file.js.map +0 -1
- package/dist/config/dist/lib/define-config.d.ts +0 -20
- package/dist/config/dist/lib/define-config.d.ts.map +0 -1
- package/dist/config/dist/lib/neon-api.d.ts +0 -375
- package/dist/config/dist/lib/neon-api.d.ts.map +0 -1
- package/dist/config/dist/lib/types.d.ts +0 -603
- package/dist/config/dist/lib/types.d.ts.map +0 -1
- package/dist/config/dist/v1.d.ts +0 -5
- package/dist/lib/cli/commands.d.ts +0 -68
- package/dist/lib/cli/commands.d.ts.map +0 -1
- package/dist/lib/cli/commands.js +0 -233
- package/dist/lib/cli/commands.js.map +0 -1
- package/dist/lib/cli/resolve-api-key.d.ts +0 -29
- package/dist/lib/cli/resolve-api-key.d.ts.map +0 -1
- package/dist/lib/cli/resolve-api-key.js +0 -74
- package/dist/lib/cli/resolve-api-key.js.map +0 -1
- package/dist/lib/cli/resolve-context.d.ts +0 -34
- package/dist/lib/cli/resolve-context.d.ts.map +0 -1
- package/dist/lib/cli/resolve-context.js +0 -88
- package/dist/lib/cli/resolve-context.js.map +0 -1
- package/dist/lib/parse-env.d.ts +0 -95
- package/dist/lib/parse-env.d.ts.map +0 -1
- package/dist/lib/parse-env.js +0 -198
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
//#region src/_shared/profiles.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* # Profiles — several Neon accounts in one config directory
|
|
4
|
-
*
|
|
5
|
-
* A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
|
|
6
|
-
* keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
|
|
7
|
-
* "active profile" state to fall out of sync, and no migration.
|
|
8
|
-
*
|
|
9
|
-
* ```
|
|
10
|
-
* ~/.config/neon/
|
|
11
|
-
* ├── credentials.json # this IS the DEFAULT profile, not a copy of it
|
|
12
|
-
* ├── credentials.work.json # created by `neon auth --profile work`
|
|
13
|
-
* └── profiles.json # created only once a second profile exists
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* `profiles.json` maps a name to a path, and the path may point anywhere — which is what
|
|
17
|
-
* makes adopting an existing directory a one-line edit rather than an import command:
|
|
18
|
-
*
|
|
19
|
-
* ```json
|
|
20
|
-
* {
|
|
21
|
-
* "version": 1,
|
|
22
|
-
* "profiles": {
|
|
23
|
-
* "DEFAULT": { "credentials": "credentials.json" },
|
|
24
|
-
* "work": {
|
|
25
|
-
* "credentials": "../neonctl-databricks/credentials.json",
|
|
26
|
-
* "label": "someone@example.com"
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* ## Selection
|
|
33
|
-
*
|
|
34
|
-
* `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
|
|
35
|
-
* `profile use` command, so nothing persists that could disagree with what you typed.
|
|
36
|
-
*
|
|
37
|
-
* ## Compatibility
|
|
38
|
-
*
|
|
39
|
-
* An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
|
|
40
|
-
* resolves to `credentials.json` in the config directory (including an existing one in the
|
|
41
|
-
* legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
|
|
42
|
-
* profile is, and nothing is ever moved.
|
|
43
|
-
*/
|
|
44
|
-
declare const PROFILES_FILE = "profiles.json";
|
|
45
|
-
/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
|
|
46
|
-
declare const DEFAULT_PROFILE = "DEFAULT";
|
|
47
|
-
type ProfileEntry = {
|
|
48
|
-
/** Path to the credentials file, relative to `profiles.json` or absolute. */
|
|
49
|
-
credentials: string;
|
|
50
|
-
/** Account email, captured at login. Display only. */
|
|
51
|
-
label?: string;
|
|
52
|
-
/** Neon user id, captured at login. Display only. */
|
|
53
|
-
userId?: string;
|
|
54
|
-
};
|
|
55
|
-
type ProfilesFile = {
|
|
56
|
-
version: 1;
|
|
57
|
-
profiles: Record<string, ProfileEntry>;
|
|
58
|
-
};
|
|
59
|
-
type ResolvedProfile = {
|
|
60
|
-
name: string;
|
|
61
|
-
/** Absolute path to this profile's credentials file. */
|
|
62
|
-
credentialsPath: string;
|
|
63
|
-
label?: string;
|
|
64
|
-
userId?: string;
|
|
65
|
-
/** True when the profile comes from `profiles.json` rather than the implicit default. */
|
|
66
|
-
declared: boolean;
|
|
67
|
-
};
|
|
68
|
-
/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
|
|
69
|
-
declare const selectProfileName: (flag?: string, env?: NodeJS.ProcessEnv) => string;
|
|
70
|
-
declare const assertValidProfileName: (name: string) => void;
|
|
71
|
-
/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
|
|
72
|
-
declare const profilesFilePath: (dir: string) => string;
|
|
73
|
-
/** What is at `profiles.json`: nothing, something readable, or something broken. */
|
|
74
|
-
type ProfilesRead = {
|
|
75
|
-
kind: "ok";
|
|
76
|
-
file: ProfilesFile;
|
|
77
|
-
} | {
|
|
78
|
-
kind: "absent";
|
|
79
|
-
}
|
|
80
|
-
/** The file is there and cannot be trusted. `reason` names the file and is safe to print. */ | {
|
|
81
|
-
kind: "unusable";
|
|
82
|
-
reason: string;
|
|
83
|
-
};
|
|
84
|
-
/**
|
|
85
|
-
* Read and classify `profiles.json` without deciding what to do about it.
|
|
86
|
-
*
|
|
87
|
-
* Entry keys and shapes are validated here rather than at each use. A key is a profile name,
|
|
88
|
-
* and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
|
|
89
|
-
* it would travel into error messages as a recovery command nobody can run, and into a
|
|
90
|
-
* `credentials.<name>.json` filename.
|
|
91
|
-
*/
|
|
92
|
-
declare const inspectProfiles: (dir: string) => ProfilesRead;
|
|
93
|
-
/**
|
|
94
|
-
* Read `profiles.json`, or `null` when there is nothing usable there.
|
|
95
|
-
*
|
|
96
|
-
* A malformed file is reported through `onWarn` and treated as absent, because for a *read* the
|
|
97
|
-
* worst case is a named profile turning up missing, which is recoverable — whereas throwing
|
|
98
|
-
* would lock the user out of `neon auth` itself. Writing is the opposite: see
|
|
99
|
-
* {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.
|
|
100
|
-
*/
|
|
101
|
-
declare const readProfiles: (dir: string, /** Called with the reason a profiles file was ignored. The consumer owns how it reports. */
|
|
102
|
-
onWarn?: (message: string) => void) => ProfilesFile | null;
|
|
103
|
-
/**
|
|
104
|
-
* Refuse to act on a named profile when the file that defines it cannot be read.
|
|
105
|
-
*
|
|
106
|
-
* Call this **before** anything that writes a credential, opens a browser, or spends an API
|
|
107
|
-
* call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already
|
|
108
|
-
* overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth
|
|
109
|
-
* --profile` has already signed in over it — a refusal that arrives after the destruction it
|
|
110
|
-
* exists to prevent. The path resolution itself is the unsound part, since with the metadata
|
|
111
|
-
* unreadable the conventional filename is a guess about which account that file belongs to.
|
|
112
|
-
*
|
|
113
|
-
* `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so
|
|
114
|
-
* signing in normally must keep working while a broken `profiles.json` is repaired.
|
|
115
|
-
*/
|
|
116
|
-
declare const assertProfilesUsable: (dir: string, name: string) => void;
|
|
117
|
-
/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
|
|
118
|
-
declare const resolveProfile: (dir: string, name: string) => ResolvedProfile;
|
|
119
|
-
/** Default location for a new named profile's credentials file. */
|
|
120
|
-
declare const newProfileCredentialsPath: (dir: string, name: string) => string;
|
|
121
|
-
/**
|
|
122
|
-
* Record a profile, creating `profiles.json` if this is the first named one.
|
|
123
|
-
*
|
|
124
|
-
* When the file is created, `DEFAULT` is written explicitly and pointed at wherever
|
|
125
|
-
* `credentials.json` actually is. That matters for an install predating the directory
|
|
126
|
-
* rename: `profiles.json` is created in `neon/` while the credentials are still in
|
|
127
|
-
* `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a
|
|
128
|
-
* relative name that would resolve to a file that isn't there.
|
|
129
|
-
*/
|
|
130
|
-
declare const upsertProfile: (dir: string, name: string, entry: {
|
|
131
|
-
credentials: string;
|
|
132
|
-
label?: string;
|
|
133
|
-
userId?: string;
|
|
134
|
-
}) => void;
|
|
135
|
-
/** Remove an entry. Returns false when it wasn't there. */
|
|
136
|
-
declare const removeProfileEntry: (dir: string, name: string) => boolean;
|
|
137
|
-
/**
|
|
138
|
-
* True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors
|
|
139
|
-
* lazy creation: a single-account install has no profiles file, before or after.
|
|
140
|
-
*/
|
|
141
|
-
declare const onlyDefaultRemains: (file: ProfilesFile) => boolean;
|
|
142
|
-
declare const listProfiles: (dir: string) => ResolvedProfile[];
|
|
143
|
-
//#endregion
|
|
144
|
-
export { DEFAULT_PROFILE, PROFILES_FILE, ProfileEntry, ProfilesFile, ProfilesRead, ResolvedProfile, assertProfilesUsable, assertValidProfileName, inspectProfiles, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, removeProfileEntry, resolveProfile, selectProfileName, upsertProfile };
|
|
145
|
-
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","names":[],"sources":["../../src/_shared/profiles.ts"],"mappings":";;AAgDA;AAGA;AAKA;AASA;AAAwB;AAEE;AAAf;AAAM;AAGjB;AAWA;AAKA;AASA;AAIA;AAcA;AAyDA;AAwBA;AAWA;AAyCA;AAYA;AA0CA;AAaA;AAQA;;;;;;;;;;;;;;;;;;;;cAjRa,aAAA;;cAGA,eAAA;KAKD,YAAA;;;;;;;;KASA,YAAA;;YAED,eAAe;;KAGd,eAAA;;;;;;;;;;cAWC,yCAEP,MAAA,CAAO;cAGA;;cASA;;KAID,YAAA;;QACW;;;;;;;;;;;;;;;;cAaV,kCAAiC;;;;;;;;;cAyDjC;uCAIV;;;;;;;;;;;;;;cAoBU;;cAWA,+CAA8C;;cAyC9C;;;;;;;;;;cAYA;;;;;;cA0CA;;;;;cAaA,2BAA4B;cAQ5B,+BAA8B"}
|
package/dist/_shared/profiles.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
|
|
2
|
-
import { writeSecretFile } from "./secure_file.js";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
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";
|
|
49
|
-
/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
|
|
50
|
-
const DEFAULT_PROFILE = "DEFAULT";
|
|
51
|
-
/** Profile names become part of a filename, so keep them boring. */
|
|
52
|
-
const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
53
|
-
/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
|
|
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.`);
|
|
57
|
-
};
|
|
58
|
-
/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
|
|
59
|
-
const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
60
|
-
/**
|
|
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
|
-
};
|
|
102
|
-
};
|
|
103
|
-
/**
|
|
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;
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
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.`);
|
|
134
|
-
};
|
|
135
|
-
/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
|
|
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}\`.`);
|
|
155
|
-
};
|
|
156
|
-
/** Default location for a new named profile's credentials file. */
|
|
157
|
-
const newProfileCredentialsPath = (dir, name) => resolve(dir, `credentials.${name}.json`);
|
|
158
|
-
/**
|
|
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);
|
|
182
|
-
};
|
|
183
|
-
/** Remove an entry. Returns false when it wasn't there. */
|
|
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;
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
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";
|
|
199
|
-
};
|
|
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));
|
|
208
|
-
};
|
|
209
|
-
const writeProfiles = (path, file) => {
|
|
210
|
-
writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
211
|
-
};
|
|
212
|
-
const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
|
|
213
|
-
/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
|
|
214
|
-
const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
|
|
215
|
-
/** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */
|
|
216
|
-
const relativeToProfiles = (profilesPath, target) => {
|
|
217
|
-
const rel = relative(resolve(profilesPath, ".."), target);
|
|
218
|
-
return rel && !isAbsolute(rel) ? rel : target;
|
|
219
|
-
};
|
|
220
|
-
function nonEmpty(value) {
|
|
221
|
-
if (typeof value !== "string") return void 0;
|
|
222
|
-
const trimmed = value.trim();
|
|
223
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
224
|
-
}
|
|
225
|
-
//#endregion
|
|
226
|
-
export { DEFAULT_PROFILE, PROFILES_FILE, assertProfilesUsable, assertValidProfileName, inspectProfiles, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, removeProfileEntry, resolveProfile, selectProfileName, upsertProfile };
|
|
227
|
-
|
|
228
|
-
//# sourceMappingURL=profiles.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.js","names":[],"sources":["../../src/_shared/profiles.ts"],"sourcesContent":["/**\n * # Profiles — several Neon accounts in one config directory\n *\n * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what\n * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent\n * \"active profile\" state to fall out of sync, and no migration.\n *\n * ```\n * ~/.config/neon/\n * ├── credentials.json # this IS the DEFAULT profile, not a copy of it\n * ├── credentials.work.json # created by `neon auth --profile work`\n * └── profiles.json # created only once a second profile exists\n * ```\n *\n * `profiles.json` maps a name to a path, and the path may point anywhere — which is what\n * makes adopting an existing directory a one-line edit rather than an import command:\n *\n * ```json\n * {\n * \"version\": 1,\n * \"profiles\": {\n * \"DEFAULT\": { \"credentials\": \"credentials.json\" },\n * \"work\": {\n * \"credentials\": \"../neonctl-databricks/credentials.json\",\n * \"label\": \"someone@example.com\"\n * }\n * }\n * }\n * ```\n *\n * ## Selection\n *\n * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no\n * `profile use` command, so nothing persists that could disagree with what you typed.\n *\n * ## Compatibility\n *\n * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`\n * resolves to `credentials.json` in the config directory (including an existing one in the\n * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second\n * profile is, and nothing is ever moved.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { credentialsPath, defaultDir, resolveConfigFile } from \"./paths.js\";\nimport { writeSecretFile } from \"./secure_file.js\";\n\nexport const PROFILES_FILE = \"profiles.json\";\n\n/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */\nexport const DEFAULT_PROFILE = \"DEFAULT\";\n\n/** Profile names become part of a filename, so keep them boring. */\nconst NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport type ProfileEntry = {\n\t/** Path to the credentials file, relative to `profiles.json` or absolute. */\n\tcredentials: string;\n\t/** Account email, captured at login. Display only. */\n\tlabel?: string;\n\t/** Neon user id, captured at login. Display only. */\n\tuserId?: string;\n};\n\nexport type ProfilesFile = {\n\tversion: 1;\n\tprofiles: Record<string, ProfileEntry>;\n};\n\nexport type ResolvedProfile = {\n\tname: string;\n\t/** Absolute path to this profile's credentials file. */\n\tcredentialsPath: string;\n\tlabel?: string;\n\tuserId?: string;\n\t/** True when the profile comes from `profiles.json` rather than the implicit default. */\n\tdeclared: boolean;\n};\n\n/** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */\nexport const selectProfileName = (\n\tflag?: string,\n\tenv: NodeJS.ProcessEnv = process.env,\n): string => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? DEFAULT_PROFILE;\n\nexport const assertValidProfileName = (name: string): void => {\n\tif (!NAME_PATTERN.test(name)) {\n\t\tthrow new Error(\n\t\t\t`Invalid profile name \"${name}\". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`,\n\t\t);\n\t}\n};\n\n/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */\nexport const profilesFilePath = (dir: string): string =>\n\tresolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/** What is at `profiles.json`: nothing, something readable, or something broken. */\nexport type ProfilesRead =\n\t| { kind: \"ok\"; file: ProfilesFile }\n\t| { kind: \"absent\" }\n\t/** The file is there and cannot be trusted. `reason` names the file and is safe to print. */\n\t| { kind: \"unusable\"; reason: string };\n\n/**\n * Read and classify `profiles.json` without deciding what to do about it.\n *\n * Entry keys and shapes are validated here rather than at each use. A key is a profile name,\n * and a name that `assertValidProfileName` would reject cannot have been written by this CLI —\n * it would travel into error messages as a recovery command nobody can run, and into a\n * `credentials.<name>.json` filename.\n */\nexport const inspectProfiles = (dir: string): ProfilesRead => {\n\tconst path = profilesFilePath(dir);\n\tif (!existsSync(path)) return { kind: \"absent\" };\n\tconst broken = (why: string): ProfilesRead => ({\n\t\tkind: \"unusable\",\n\t\treason: `${path} could not be read as a profiles file: ${why}`,\n\t});\n\t// Reading and parsing are separate failures with separate answers. Sharing one catch\n\t// reported `EACCES` as \"not valid JSON\", which sends the user to edit a file that is\n\t// perfectly valid and that they cannot open.\n\tlet contents: string;\n\ttry {\n\t\tcontents = readFileSync(path, \"utf8\");\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn broken(\n\t\t\tcode ? `reading it failed with ${code}` : \"reading it failed\",\n\t\t);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(contents);\n\t} catch {\n\t\treturn broken(\"it is not valid JSON\");\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn broken(\"it does not contain an object\");\n\tconst profiles = (parsed as ProfilesFile).profiles;\n\tif (\n\t\tprofiles === null ||\n\t\ttypeof profiles !== \"object\" ||\n\t\tArray.isArray(profiles)\n\t)\n\t\treturn broken(\"it has no `profiles` object\");\n\tfor (const [name, entry] of Object.entries(profiles)) {\n\t\tif (!NAME_PATTERN.test(name))\n\t\t\treturn broken(`\"${name}\" is not a valid profile name`);\n\t\tif (\n\t\t\tentry === null ||\n\t\t\ttypeof entry !== \"object\" ||\n\t\t\ttypeof entry.credentials !== \"string\" ||\n\t\t\tentry.credentials.trim() === \"\"\n\t\t) {\n\t\t\treturn broken(`profile \"${name}\" has no \\`credentials\\` path`);\n\t\t}\n\t}\n\treturn { kind: \"ok\", file: { version: 1, profiles } };\n};\n\n/**\n * Read `profiles.json`, or `null` when there is nothing usable there.\n *\n * A malformed file is reported through `onWarn` and treated as absent, because for a *read* the\n * worst case is a named profile turning up missing, which is recoverable — whereas throwing\n * would lock the user out of `neon auth` itself. Writing is the opposite: see\n * {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.\n */\nexport const readProfiles = (\n\tdir: string,\n\t/** Called with the reason a profiles file was ignored. The consumer owns how it reports. */\n\tonWarn: (message: string) => void = () => {},\n): ProfilesFile | null => {\n\tconst read = inspectProfiles(dir);\n\tif (read.kind === \"ok\") return read.file;\n\tif (read.kind === \"unusable\") onWarn(read.reason);\n\treturn null;\n};\n\n/**\n * Refuse to act on a named profile when the file that defines it cannot be read.\n *\n * Call this **before** anything that writes a credential, opens a browser, or spends an API\n * call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already\n * overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth\n * --profile` has already signed in over it — a refusal that arrives after the destruction it\n * exists to prevent. The path resolution itself is the unsound part, since with the metadata\n * unreadable the conventional filename is a guess about which account that file belongs to.\n *\n * `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so\n * signing in normally must keep working while a broken `profiles.json` is repaired.\n */\nexport const assertProfilesUsable = (dir: string, name: string): void => {\n\tif (name === DEFAULT_PROFILE) return;\n\tconst read = inspectProfiles(dir);\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file before working with profile \"${name}\" — it is the only record of where each account's credentials live.`,\n\t\t);\n\t}\n};\n\n/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */\nexport const resolveProfile = (dir: string, name: string): ResolvedProfile => {\n\tconst read = inspectProfiles(dir);\n\t// A broken file must not be reported as `Unknown profile \"work\"`. That names the wrong\n\t// problem, and the user goes looking for a profile they can see in the file in front of them.\n\tif (read.kind === \"unusable\" && name !== DEFAULT_PROFILE) {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file — every named profile is defined in it.`,\n\t\t);\n\t}\n\tconst file = read.kind === \"ok\" ? read.file : null;\n\tconst entry = file?.profiles[name];\n\n\tif (entry) {\n\t\treturn {\n\t\t\tname,\n\t\t\tcredentialsPath: resolveEntryPath(dir, entry.credentials),\n\t\t\t...(entry.label ? { label: entry.label } : {}),\n\t\t\t...(entry.userId ? { userId: entry.userId } : {}),\n\t\t\tdeclared: true,\n\t\t};\n\t}\n\n\t// DEFAULT works with no profiles.json at all, and keeps working when one exists but\n\t// doesn't mention it — that is the pre-profiles behaviour, unchanged.\n\tif (name === DEFAULT_PROFILE) {\n\t\treturn {\n\t\t\tname,\n\t\t\tcredentialsPath: credentialsPath(dir),\n\t\t\tdeclared: false,\n\t\t};\n\t}\n\n\tconst known = file\n\t\t? Object.keys(file.profiles).join(\", \")\n\t\t: DEFAULT_PROFILE;\n\tthrow new Error(\n\t\t`Unknown profile \"${name}\". Known profiles: ${known}. Create it with \\`neon profile create ${name}\\`.`,\n\t);\n};\n\n/** Default location for a new named profile's credentials file. */\nexport const newProfileCredentialsPath = (dir: string, name: string): string =>\n\tresolve(dir, `credentials.${name}.json`);\n\n/**\n * Record a profile, creating `profiles.json` if this is the first named one.\n *\n * When the file is created, `DEFAULT` is written explicitly and pointed at wherever\n * `credentials.json` actually is. That matters for an install predating the directory\n * rename: `profiles.json` is created in `neon/` while the credentials are still in\n * `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a\n * relative name that would resolve to a file that isn't there.\n */\nexport const upsertProfile = (\n\tdir: string,\n\tname: string,\n\tentry: { credentials: string; label?: string; userId?: string },\n): void => {\n\tassertValidProfileName(name);\n\tconst path = profilesFilePath(dir);\n\tconst read = inspectProfiles(dir);\n\t// Refusing is the point. Treating a broken file as absent here rebuilt it from a single\n\t// `DEFAULT` entry and dropped every named profile in it — silent data loss, in the file\n\t// that is the only record of where each account's credentials live. The credentials\n\t// themselves survive, so fixing the file by hand recovers everything.\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`,\n\t\t);\n\t}\n\tconst file =\n\t\tread.kind === \"ok\"\n\t\t\t? read.file\n\t\t\t: {\n\t\t\t\t\tversion: 1 as const,\n\t\t\t\t\tprofiles: {\n\t\t\t\t\t\t[DEFAULT_PROFILE]: {\n\t\t\t\t\t\t\tcredentials: relativeToProfiles(\n\t\t\t\t\t\t\t\tpath,\n\t\t\t\t\t\t\t\tcredentialsPath(dir),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t};\n\n\tfile.profiles[name] = {\n\t\tcredentials: relativeToProfiles(path, entry.credentials),\n\t\t...(entry.label ? { label: entry.label } : {}),\n\t\t...(entry.userId ? { userId: entry.userId } : {}),\n\t};\n\n\twriteProfiles(path, file);\n};\n\n/** Remove an entry. Returns false when it wasn't there. */\nexport const removeProfileEntry = (dir: string, name: string): boolean => {\n\tconst path = profilesFilePath(dir);\n\tconst file = readProfiles(dir);\n\tif (!file?.profiles[name]) return false;\n\tdelete file.profiles[name];\n\twriteProfiles(path, file);\n\treturn true;\n};\n\n/**\n * True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors\n * lazy creation: a single-account install has no profiles file, before or after.\n */\nexport const onlyDefaultRemains = (file: ProfilesFile): boolean => {\n\tconst names = Object.keys(file.profiles);\n\treturn (\n\t\tnames.length === 0 ||\n\t\t(names.length === 1 && names[0] === DEFAULT_PROFILE)\n\t);\n};\n\nexport const listProfiles = (dir: string): ResolvedProfile[] => {\n\tconst read = inspectProfiles(dir);\n\t// Listing is the command run to find out what is there, so a broken file is the answer\n\t// rather than an obstacle. Showing only `DEFAULT` would state, as fact, that the profiles\n\t// in that file do not exist.\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(\n\t\t\t`${read.reason}. Fix or delete the file — every named profile is defined in it.`,\n\t\t);\n\t}\n\tconst file = read.kind === \"ok\" ? read.file : null;\n\tif (!file) return [resolveProfile(dir, DEFAULT_PROFILE)];\n\tconst names = Object.keys(file.profiles);\n\tif (!names.includes(DEFAULT_PROFILE)) names.unshift(DEFAULT_PROFILE);\n\treturn names.map((name) => resolveProfile(dir, name));\n};\n\nconst writeProfiles = (path: string, file: ProfilesFile): void => {\n\twriteSecretFile(path, `${JSON.stringify(file, null, 2)}\\n`);\n};\n\nconst resolveEntryPath = (dir: string, entry: string): string =>\n\tisAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);\n\n/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */\nconst profilesDir = (dir: string): string =>\n\tresolve(profilesFilePath(dir), \"..\");\n\n/** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */\nconst relativeToProfiles = (profilesPath: string, target: string): string => {\n\tconst rel = relative(resolve(profilesPath, \"..\"), target);\n\treturn rel && !isAbsolute(rel) ? rel : target;\n};\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed === \"\" ? undefined : trimmed;\n}\n"],"mappings":"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"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
//#region src/_shared/secure_file.d.ts
|
|
2
|
-
/** Owner read/write. A credential needs those two and nothing else. */
|
|
3
|
-
declare const SECRET_FILE_MODE = 384;
|
|
4
|
-
/**
|
|
5
|
-
* Write a secret to disk owner-only, by creating a temporary file in the same directory and
|
|
6
|
-
* renaming it over the target.
|
|
7
|
-
*
|
|
8
|
-
* The rename is what makes this correct rather than merely tidy. `writeFileSync`'s `mode`
|
|
9
|
-
* applies only when it *creates* the file, so writing over an existing credentials file
|
|
10
|
-
* leaves whatever permissions it already had — a file created `0700` by an older release
|
|
11
|
-
* stays `0700` forever, and one created before a umask change stays world-readable. Renaming
|
|
12
|
-
* a fresh inode into place means every write lands at {@link SECRET_FILE_MODE}, so the
|
|
13
|
-
* permissions repair themselves instead of being inherited.
|
|
14
|
-
*
|
|
15
|
-
* It also closes the window where a reader could see the file at default permissions: the
|
|
16
|
-
* temporary file is created `0600` *before* it holds the secret's final name, and `rename`
|
|
17
|
-
* is atomic within a directory, so there is no moment at which the target is readable by
|
|
18
|
-
* anyone else and no moment at which it is half-written.
|
|
19
|
-
*
|
|
20
|
-
* The temporary name carries the pid so two processes writing at once cannot collide on it.
|
|
21
|
-
*/
|
|
22
|
-
declare const writeSecretFile: (path: string, contents: string) => void;
|
|
23
|
-
//#endregion
|
|
24
|
-
export { SECRET_FILE_MODE, writeSecretFile };
|
|
25
|
-
//# sourceMappingURL=secure_file.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"secure_file.d.ts","names":[],"sources":["../../src/_shared/secure_file.ts"],"mappings":";;AAIa,cAAA,gBAAA,GAAgB,GAAA;AAoB7B;;;;;;;;;;;;;;;;;;cAAa"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
-
//#region src/_shared/secure_file.ts
|
|
4
|
-
/** Owner read/write. A credential needs those two and nothing else. */
|
|
5
|
-
const SECRET_FILE_MODE = 384;
|
|
6
|
-
/**
|
|
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
|
-
}
|
|
39
|
-
};
|
|
40
|
-
//#endregion
|
|
41
|
-
export { SECRET_FILE_MODE, writeSecretFile };
|
|
42
|
-
|
|
43
|
-
//# sourceMappingURL=secure_file.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"secure_file.js","names":[],"sources":["../../src/_shared/secure_file.ts"],"sourcesContent":["import { renameSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\n\n/** Owner read/write. A credential needs those two and nothing else. */\nexport const SECRET_FILE_MODE = 0o600;\n\n/**\n * Write a secret to disk owner-only, by creating a temporary file in the same directory and\n * renaming it over the target.\n *\n * The rename is what makes this correct rather than merely tidy. `writeFileSync`'s `mode`\n * applies only when it *creates* the file, so writing over an existing credentials file\n * leaves whatever permissions it already had — a file created `0700` by an older release\n * stays `0700` forever, and one created before a umask change stays world-readable. Renaming\n * a fresh inode into place means every write lands at {@link SECRET_FILE_MODE}, so the\n * permissions repair themselves instead of being inherited.\n *\n * It also closes the window where a reader could see the file at default permissions: the\n * temporary file is created `0600` *before* it holds the secret's final name, and `rename`\n * is atomic within a directory, so there is no moment at which the target is readable by\n * anyone else and no moment at which it is half-written.\n *\n * The temporary name carries the pid so two processes writing at once cannot collide on it.\n */\nexport const writeSecretFile = (path: string, contents: string): void => {\n\tconst directory = dirname(path);\n\tconst temporary = join(\n\t\tdirectory,\n\t\t`.${basename(path)}.${process.pid}.${Date.now()}.tmp`,\n\t);\n\ttry {\n\t\twriteFileSync(temporary, contents, {\n\t\t\tencoding: \"utf8\",\n\t\t\tmode: SECRET_FILE_MODE,\n\t\t});\n\t\trenameSync(temporary, path);\n\t} catch (err) {\n\t\t// Never leave the secret behind under a temporary name the caller doesn't know about.\n\t\ttry {\n\t\t\tunlinkSync(temporary);\n\t\t} catch {\n\t\t\t// The temp file was never created, or is already gone. Report the original error.\n\t\t}\n\t\tthrow err;\n\t}\n};\n"],"mappings":";;;;AAIA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;AAoBhC,MAAa,mBAAmB,MAAc,aAA2B;CACxE,MAAM,YAAY,QAAQ,IAAI;CAC9B,MAAM,YAAY,KACjB,WACA,IAAI,SAAS,IAAI,EAAE,GAAG,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,KACjD;CACA,IAAI;EACH,cAAc,WAAW,UAAU;GAClC,UAAU;GACV,MAAA;EACD,CAAC;EACD,WAAW,WAAW,IAAI;CAC3B,SAAS,KAAK;EAEb,IAAI;GACH,WAAW,SAAS;EACrB,QAAQ,CAER;EACA,MAAM;CACP;AACD"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { BranchTarget, Config, ResolvedBranchConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
//#region ../config/dist/lib/define-config.d.ts
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Evaluate a branch policy for a specific branch target and return a normalized config.
|
|
7
|
-
*
|
|
8
|
-
* Merges the static existential set (services + preview functions/buckets) with the
|
|
9
|
-
* per-branch tuning returned by the `branch` closure into the same {@link
|
|
10
|
-
* ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
|
|
11
|
-
*/
|
|
12
|
-
declare function resolveConfig(config: Config, branch: BranchTarget): ResolvedBranchConfig;
|
|
13
|
-
/**
|
|
14
|
-
* Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes
|
|
15
|
-
* `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the
|
|
16
|
-
* NeonApi adapter.
|
|
17
|
-
*/
|
|
18
|
-
//#endregion
|
|
19
|
-
export { resolveConfig };
|
|
20
|
-
//# sourceMappingURL=define-config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"define-config.d.ts","names":["BranchTarget","BranchTuningFn","BucketDef","Config","DataApiInput","FunctionDef","PreviewInput","ResolvedBranchConfig","ServiceEnabled","ServiceToggleInput","DataApiUsesNeonAuth","DataApi","NeonAuthRequiredHint","DataApiField","Auth","PreviewAutocomplete","Preview","F","B","defineConfig","resolveConfig","normalizeRegion"],"sources":["../../../../../config/dist/lib/define-config.d.ts"],"sourcesContent":["import { BranchTarget, BranchTuningFn, BucketDef, Config, DataApiInput, FunctionDef, PreviewInput, ResolvedBranchConfig, ServiceEnabled, ServiceToggleInput } from \"./types.js\";\n\n//#region src/lib/define-config.d.ts\n\n/**\n * Whether a `dataApi` toggle is **enabled and verified by Neon Auth** at the type level: it is\n * on (see {@link ServiceEnabled}) and not the explicit `authProvider: \"external\"` variant\n * (so the default / `\"neon\"` provider). This is the case that requires top-level Neon Auth.\n */\ntype DataApiUsesNeonAuth<DataApi> = ServiceEnabled<DataApi> extends true ? [DataApi] extends [{\n authProvider: \"external\";\n}] ? false : true : false;\n/**\n * Human-readable hint surfaced as the **expected type** of `dataApi` when a Neon-Auth Data\n * API is declared without Neon Auth enabled (see {@link DataApiField}). TypeScript prints the\n * offending value against this string literal — `Type 'true' is not assignable to type\n * '…requires `auth: true`…'` — which points straight at the fix, instead of the opaque\n * `Type 'true' is not assignable to type 'never'` an intersection guard produces.\n *\n * It documents **both** fixes: enabling Neon Auth (`auth: true`), and running the Data API\n * *without* Neon Auth by verifying a third-party IdP (`authProvider: 'external'` + `jwksUrl`).\n */\ntype NeonAuthRequiredHint = \"`dataApi` with Neon Auth (the default `authProvider: 'neon'`) requires Neon Auth, so add `auth: true`. To enable the Data API WITHOUT Neon Auth, verify a third-party IdP instead: `dataApi: { authProvider: 'external', jwksUrl: 'https://your-idp/.well-known/jwks.json' }`\";\n/**\n * Static cross-field guard for {@link defineConfig}, expressed as the **type of the `dataApi`\n * field** rather than an intersected requirement on `auth`.\n *\n * - A Neon-Auth Data API (`authProvider: \"neon\"`, the default) with top-level `auth` enabled,\n * or any external Data API: the field keeps its normal `DataApi & DataApiInput` type (the\n * `& DataApiInput` preserves member autocomplete; the `const DataApi` still types the\n * returned {@link Config}).\n * - A Neon-Auth Data API **without** `auth` enabled: the field's expected type collapses to\n * the {@link NeonAuthRequiredHint} message, so the author sees the rule (and the two fixes)\n * right on the `dataApi` value.\n *\n * The runtime `superRefine` in {@link configInputSchema} enforces the same invariant for\n * non-typed (plain-JS) callers, so the behavior is identical — only the type-level message\n * changes.\n */\ntype DataApiField<Auth, DataApi> = DataApiUsesNeonAuth<DataApi> extends true ? ServiceEnabled<Auth> extends true ? DataApi & DataApiInput : NeonAuthRequiredHint : DataApi & DataApiInput;\n/**\n * Autocomplete bridge for the nested `preview.functions` / `preview.buckets` slug objects.\n *\n * {@link PreviewInput} types those records with a string index signature\n * (`Record<string, FunctionDef>` / `Record<string, BucketDef>`). When `defineConfig` infers\n * `const Preview`, every authored slug becomes a **named** property on the inferred literal\n * (e.g. `{ hello: { name; source } }`), and a named property **shadows** the index signature\n * when the editor computes the contextual type of that slug's value — so the rest of\n * {@link FunctionDef} / {@link BucketDef} (`env`, `dev`, `access`, …) never surfaces as\n * completions inside `hello: { … }` / `uploads: { … }`.\n *\n * Re-declaring each inferred slug's value as `FunctionDef` / `BucketDef` (a *named* member, via\n * a mapped type over the already-inferred keys) puts those members back onto the contextual\n * type without going through an index signature, which restores autocomplete. Intersected with\n * `Preview & PreviewInput` it neither widens what is accepted (the values were already\n * `FunctionDef` / `BucketDef`) nor perturbs the inferred `const Preview` — so slug inference for\n * `BranchTuningFn<Preview>` and the returned {@link Config} is unchanged.\n */\ntype PreviewAutocomplete<Preview> = (Preview extends {\n functions: infer F;\n} ? {\n functions: { [Slug in keyof F]: FunctionDef };\n} : unknown) & (Preview extends {\n buckets: infer B;\n} ? {\n buckets: { [Name in keyof B]: BucketDef };\n} : unknown);\n/**\n * Validate and freeze a Neon branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neon/config/v1\";\n *\n * export default defineConfig({\n * auth: true,\n * preview: {\n * functions: {\n * hello: { name: \"Hello\", source: \"./functions/hello.ts\", dev: { port: 8787 } },\n * },\n * },\n * branch: (branch) => ({ protected: branch.name === \"main\" }),\n * });\n * ```\n *\n * The policy is split into a **static** existential set (top-level `auth` / `dataApi`\n * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The\n * static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`\n * are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function\n * deploy settings), never change what exists.\n *\n * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch\n * being decided for (not a live handle); switch on its facts (`branch.name`,\n * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two\n * modes: against an existing branch (fields populated from Neon) and during pre-create\n * evaluation (`exists: false`, `id` undefined).\n *\n * Pure: no I/O, no side effects. The static parts are validated here; the closure's output\n * is validated every time it is evaluated so errors point at the concrete branch target.\n */\ndeclare function defineConfig<const Auth extends ServiceToggleInput | undefined = undefined, const DataApi extends DataApiInput | undefined = undefined, const Preview extends PreviewInput | undefined = undefined>(input: {\n auth?: Auth & ServiceToggleInput;\n dataApi?: DataApiField<Auth, DataApi>;\n preview?: Preview & PreviewInput & PreviewAutocomplete<Preview>;\n branch?: BranchTuningFn<Preview>;\n}): Config<Auth, DataApi, Preview>;\n/**\n * Evaluate a branch policy for a specific branch target and return a normalized config.\n *\n * Merges the static existential set (services + preview functions/buckets) with the\n * per-branch tuning returned by the `branch` closure into the same {@link\n * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.\n */\ndeclare function resolveConfig(config: Config, branch: BranchTarget): ResolvedBranchConfig;\n/**\n * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes\n * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the\n * NeonApi adapter.\n */\ndeclare function normalizeRegion(region: string): string;\n//#endregion\nexport { DataApiField, NeonAuthRequiredHint, defineConfig, normalizeRegion, resolveConfig };\n//# sourceMappingURL=define-config.d.ts.map"],"mappings":";;;;;;;;;;;iBAiHiBoB,aAAAA,SAAsBjB,gBAAgBH,eAAeO"}
|