@neondatabase/env 0.15.0 → 1.0.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 +8 -29
- package/dist/_shared/auth_selection.d.ts +95 -0
- package/dist/_shared/auth_selection.d.ts.map +1 -0
- package/dist/_shared/auth_selection.js +10 -1
- package/dist/_shared/auth_selection.js.map +1 -1
- package/dist/_shared/credentials.d.ts +186 -0
- package/dist/_shared/credentials.d.ts.map +1 -0
- package/dist/_shared/credentials.js +60 -1
- package/dist/_shared/credentials.js.map +1 -1
- package/dist/{lib → _shared/env-core}/env.d.ts +7 -92
- package/dist/_shared/env-core/env.d.ts.map +1 -0
- package/dist/{lib → _shared/env-core}/env.js +14 -184
- package/dist/_shared/env-core/env.js.map +1 -0
- package/dist/{lib → _shared/env-core}/reuse-secrets.d.ts +4 -4
- package/dist/_shared/env-core/reuse-secrets.d.ts.map +1 -0
- package/dist/{lib → _shared/env-core}/reuse-secrets.js +2 -2
- package/dist/_shared/env-core/reuse-secrets.js.map +1 -0
- package/dist/_shared/paths.d.ts +116 -0
- package/dist/_shared/paths.d.ts.map +1 -0
- package/dist/_shared/paths.js +23 -1
- package/dist/_shared/paths.js.map +1 -1
- package/dist/_shared/profiles.d.ts +140 -2
- package/dist/_shared/profiles.d.ts.map +1 -1
- package/dist/_shared/profiles.js +106 -2
- package/dist/_shared/profiles.js.map +1 -1
- package/dist/_shared/secure_file.d.ts +25 -0
- package/dist/_shared/secure_file.d.ts.map +1 -0
- package/dist/_shared/secure_file.js +43 -0
- package/dist/_shared/secure_file.js.map +1 -0
- package/dist/config/dist/lib/types.d.ts +92 -33
- package/dist/config/dist/lib/types.d.ts.map +1 -1
- package/dist/config/dist/v1.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/cli/commands.js +1 -1
- package/dist/lib/cli/commands.js.map +1 -1
- package/dist/lib/parse-env.d.ts +95 -0
- package/dist/lib/parse-env.d.ts.map +1 -0
- package/dist/lib/parse-env.js +198 -0
- package/dist/lib/parse-env.js.map +1 -0
- package/package.json +4 -9
- package/dist/lib/env.d.ts.map +0 -1
- package/dist/lib/env.js.map +0 -1
- package/dist/lib/reuse-secrets.d.ts.map +0 -1
- package/dist/lib/reuse-secrets.js.map +0 -1
- package/dist/runtime.d.ts +0 -2
- package/dist/runtime.js +0 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/_shared/paths.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* # Where the Neon CLIs keep their files on disk
|
|
4
|
+
*
|
|
5
|
+
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
6
|
+
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
7
|
+
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
8
|
+
* carry implementor-only code.
|
|
9
|
+
*
|
|
10
|
+
* It exists because three separate readers each grew their own answer to "where is the
|
|
11
|
+
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
12
|
+
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
13
|
+
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
14
|
+
* credentials somewhere the other two never looked.
|
|
15
|
+
*
|
|
16
|
+
* ## The directory
|
|
17
|
+
*
|
|
18
|
+
* `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
|
|
19
|
+
* each entry winning over the next:
|
|
20
|
+
*
|
|
21
|
+
* 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
|
|
22
|
+
* 2. `NEON_CONFIG_DIR` — exact.
|
|
23
|
+
* 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
|
|
24
|
+
* 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
|
|
25
|
+
*
|
|
26
|
+
* An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
|
|
27
|
+
* quietly read `~/.config/neonctl` would defeat the point of passing it.
|
|
28
|
+
*
|
|
29
|
+
* ## The files
|
|
30
|
+
*
|
|
31
|
+
* {@link resolveConfigFile} answers "which path should I use for this file", and it is the
|
|
32
|
+
* same answer for reading and writing:
|
|
33
|
+
*
|
|
34
|
+
* - Present in `neon/` → use it.
|
|
35
|
+
* - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
|
|
36
|
+
* never copied or moved, so nothing is left behind to go stale and no other tool starts
|
|
37
|
+
* reading an abandoned token.
|
|
38
|
+
* - Present in neither → the new location. New files only ever appear under `neon/`.
|
|
39
|
+
*/
|
|
40
|
+
/** Current directory name. New files are created here. */
|
|
41
|
+
declare const CONFIG_DIR_NAME = "neon";
|
|
42
|
+
/** Legacy directory name, read forever so existing installs keep working untouched. */
|
|
43
|
+
declare const LEGACY_CONFIG_DIR_NAME = "neonctl";
|
|
44
|
+
interface ConfigPathOptions {
|
|
45
|
+
/**
|
|
46
|
+
* An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no
|
|
47
|
+
* environment variables are consulted and the legacy directory is never searched.
|
|
48
|
+
*/
|
|
49
|
+
dir?: string;
|
|
50
|
+
/** Environment to read. Defaults to `process.env`. Injectable for tests. */
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
}
|
|
53
|
+
/** Where files are created. See the module docs for the precedence. */
|
|
54
|
+
declare function configDir(options?: ConfigPathOptions): string;
|
|
55
|
+
/**
|
|
56
|
+
* The legacy directory, or `undefined` when the location was chosen explicitly (in which
|
|
57
|
+
* case there is no legacy counterpart to fall back to).
|
|
58
|
+
*/
|
|
59
|
+
declare function legacyConfigDir(options?: ConfigPathOptions): string | undefined;
|
|
60
|
+
interface ResolvedConfigFile {
|
|
61
|
+
/** The path to use, for both reading and writing. */
|
|
62
|
+
path: string;
|
|
63
|
+
/** The directory `path` lives in. */
|
|
64
|
+
dir: string;
|
|
65
|
+
/** True when the file was found in the legacy `neonctl` directory. */
|
|
66
|
+
isLegacy: boolean;
|
|
67
|
+
/** Whether the file exists at `path` right now. */
|
|
68
|
+
exists: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve one file inside the config directory. Prefers the current location, falls back to
|
|
72
|
+
* an existing legacy file **in place**, and otherwise points at the current location so new
|
|
73
|
+
* files are created there.
|
|
74
|
+
*/
|
|
75
|
+
declare function resolveConfigFile(fileName: string, options?: ConfigPathOptions): ResolvedConfigFile;
|
|
76
|
+
declare const CREDENTIALS_FILE = "credentials.json";
|
|
77
|
+
/**
|
|
78
|
+
* Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
|
|
79
|
+
*
|
|
80
|
+
* The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
|
|
81
|
+
* see {@link credentialsPath} — but it is never written to, moved, or deleted.
|
|
82
|
+
*/
|
|
83
|
+
declare const defaultDir: string;
|
|
84
|
+
/**
|
|
85
|
+
* Where this invocation's `credentials.json` lives.
|
|
86
|
+
*
|
|
87
|
+
* When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
|
|
88
|
+
* directory is used **in place**: an install that predates the rename keeps working, and its
|
|
89
|
+
* credentials are never duplicated into a second location where one copy could go stale while
|
|
90
|
+
* another tool still reads it.
|
|
91
|
+
*
|
|
92
|
+
* A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
|
|
93
|
+
* explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
|
|
94
|
+
* scratch directory must never pick up a developer's real credentials.
|
|
95
|
+
*/
|
|
96
|
+
declare const credentialsPath: (dir: string) => string;
|
|
97
|
+
/**
|
|
98
|
+
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
99
|
+
*
|
|
100
|
+
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
101
|
+
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
102
|
+
* create is not ours to remove.
|
|
103
|
+
*/
|
|
104
|
+
declare const isInsideConfigDir: (configDirectory: string, file: string) => boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
107
|
+
*
|
|
108
|
+
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
109
|
+
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
110
|
+
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
111
|
+
* the rename "adopted".
|
|
112
|
+
*/
|
|
113
|
+
declare const isOwnedCredentialPath: (configDirectory: string, file: string) => boolean;
|
|
114
|
+
//#endregion
|
|
115
|
+
export { CONFIG_DIR_NAME, CREDENTIALS_FILE, ConfigPathOptions, LEGACY_CONFIG_DIR_NAME, ResolvedConfigFile, configDir, credentialsPath, defaultDir, isInsideConfigDir, isOwnedCredentialPath, legacyConfigDir, resolveConfigFile };
|
|
116
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","names":[],"sources":["../../src/_shared/paths.ts"],"mappings":";;AA2CA;AAGA;AAEA;AAWA;AAUA;AAOA;AAgBA;AAAiC;AAEvB;AACP;AAAkB;AA4CrB;AAQA;AAcA;AAUA;AAaA;;;;;;;;;;;;;;;;;;;;;;;cA7Ia,eAAA;;cAGA,sBAAA;UAEI,iBAAA;;;;;;;QAOV,MAAA,CAAO;;;iBAIE,SAAA,WAAmB;;;;;iBAUnB,eAAA,WACN;UAMO,kBAAA;;;;;;;;;;;;;;;iBAgBD,iBAAA,6BAEN,oBACP;cA4CU,gBAAA;;;;;;;cAQA;;;;;;;;;;;;;cAcA;;;;;;;;cAUA;;;;;;;;;cAaA"}
|
package/dist/_shared/paths.js
CHANGED
|
@@ -125,7 +125,29 @@ const defaultDir = configDir();
|
|
|
125
125
|
* scratch directory must never pick up a developer's real credentials.
|
|
126
126
|
*/
|
|
127
127
|
const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
128
|
+
/**
|
|
129
|
+
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
130
|
+
*
|
|
131
|
+
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
132
|
+
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
133
|
+
* create is not ours to remove.
|
|
134
|
+
*/
|
|
135
|
+
const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
|
|
136
|
+
/**
|
|
137
|
+
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
138
|
+
*
|
|
139
|
+
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
140
|
+
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
141
|
+
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
142
|
+
* the rename "adopted".
|
|
143
|
+
*/
|
|
144
|
+
const isOwnedCredentialPath = (configDirectory, file) => {
|
|
145
|
+
if (isInsideConfigDir(configDirectory, file)) return true;
|
|
146
|
+
if (configDirectory !== defaultDir) return false;
|
|
147
|
+
const legacy = legacyConfigDir();
|
|
148
|
+
return legacy !== void 0 && isInsideConfigDir(legacy, file);
|
|
149
|
+
};
|
|
128
150
|
//#endregion
|
|
129
|
-
export { CONFIG_DIR_NAME, CREDENTIALS_FILE, LEGACY_CONFIG_DIR_NAME, configDir, credentialsPath, defaultDir, legacyConfigDir, resolveConfigFile };
|
|
151
|
+
export { CONFIG_DIR_NAME, CREDENTIALS_FILE, LEGACY_CONFIG_DIR_NAME, configDir, credentialsPath, defaultDir, isInsideConfigDir, isOwnedCredentialPath, legacyConfigDir, resolveConfigFile };
|
|
130
152
|
|
|
131
153
|
//# sourceMappingURL=paths.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","names":[],"sources":["../../src/_shared/paths.ts"],"sourcesContent":["/**\n * # Where the Neon CLIs keep their files on disk\n *\n **Deliberately impure.** It reads environment variables and touches the filesystem, which\n * `@neon/config` — the package this used to be a subpath of — must never do from its root\n * export. It lives here instead of there precisely so that a policy-facing package does not\n * carry implementor-only code.\n *\n * It exists because three separate readers each grew their own answer to \"where is the\n * config directory\", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but\n * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init\n * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote\n * credentials somewhere the other two never looked.\n *\n * ## The directory\n *\n * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,\n * each entry winning over the next:\n *\n * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.\n * 2. `NEON_CONFIG_DIR` — exact.\n * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.\n * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.\n *\n * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that\n * quietly read `~/.config/neonctl` would defeat the point of passing it.\n *\n * ## The files\n *\n * {@link resolveConfigFile} answers \"which path should I use for this file\", and it is the\n * same answer for reading and writing:\n *\n * - Present in `neon/` → use it.\n * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is\n * never copied or moved, so nothing is left behind to go stale and no other tool starts\n * reading an abandoned token.\n * - Present in neither → the new location. New files only ever appear under `neon/`.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\n/** Current directory name. New files are created here. */\nexport const CONFIG_DIR_NAME = \"neon\";\n\n/** Legacy directory name, read forever so existing installs keep working untouched. */\nexport const LEGACY_CONFIG_DIR_NAME = \"neonctl\";\n\nexport interface ConfigPathOptions {\n\t/**\n\t * An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no\n\t * environment variables are consulted and the legacy directory is never searched.\n\t */\n\tdir?: string;\n\t/** Environment to read. Defaults to `process.env`. Injectable for tests. */\n\tenv?: NodeJS.ProcessEnv;\n}\n\n/** Where files are created. See the module docs for the precedence. */\nexport function configDir(options: ConfigPathOptions = {}): string {\n\tconst explicit = explicitDir(options);\n\tif (explicit) return explicit;\n\treturn join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);\n}\n\n/**\n * The legacy directory, or `undefined` when the location was chosen explicitly (in which\n * case there is no legacy counterpart to fall back to).\n */\nexport function legacyConfigDir(\n\toptions: ConfigPathOptions = {},\n): string | undefined {\n\tif (explicitDir(options)) return undefined;\n\treturn join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);\n}\n\nexport interface ResolvedConfigFile {\n\t/** The path to use, for both reading and writing. */\n\tpath: string;\n\t/** The directory `path` lives in. */\n\tdir: string;\n\t/** True when the file was found in the legacy `neonctl` directory. */\n\tisLegacy: boolean;\n\t/** Whether the file exists at `path` right now. */\n\texists: boolean;\n}\n\n/**\n * Resolve one file inside the config directory. Prefers the current location, falls back to\n * an existing legacy file **in place**, and otherwise points at the current location so new\n * files are created there.\n */\nexport function resolveConfigFile(\n\tfileName: string,\n\toptions: ConfigPathOptions = {},\n): ResolvedConfigFile {\n\tconst dir = configDir(options);\n\tconst current = resolve(dir, fileName);\n\tif (existsSync(current))\n\t\treturn { path: current, dir, isLegacy: false, exists: true };\n\n\tconst legacyDir = legacyConfigDir(options);\n\tif (legacyDir) {\n\t\tconst legacy = resolve(legacyDir, fileName);\n\t\tif (existsSync(legacy))\n\t\t\treturn {\n\t\t\t\tpath: legacy,\n\t\t\t\tdir: legacyDir,\n\t\t\t\tisLegacy: true,\n\t\t\t\texists: true,\n\t\t\t};\n\t}\n\n\treturn { path: current, dir, isLegacy: false, exists: false };\n}\n\n/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */\nfunction configHome(env: NodeJS.ProcessEnv): string {\n\tconst xdg = nonEmpty(env.XDG_CONFIG_HOME);\n\tif (xdg) return xdg;\n\tconst home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);\n\treturn home ? join(home, \".config\") : \".config\";\n}\n\nfunction explicitDir(options: ConfigPathOptions): string | undefined {\n\tconst env = options.env ?? process.env;\n\treturn (\n\t\tnonEmpty(options.dir) ??\n\t\tnonEmpty(env.NEON_CONFIG_DIR) ??\n\t\tnonEmpty(env.NEONCTL_CONFIG_DIR)\n\t);\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\nexport const CREDENTIALS_FILE = \"credentials.json\";\n\n/**\n * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.\n *\n * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —\n * see {@link credentialsPath} — but it is never written to, moved, or deleted.\n */\nexport const defaultDir = configDir();\n\n/**\n * Where this invocation's `credentials.json` lives.\n *\n * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`\n * directory is used **in place**: an install that predates the rename keeps working, and its\n * credentials are never duplicated into a second location where one copy could go stale while\n * another tool still reads it.\n *\n * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an\n * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a\n * scratch directory must never pick up a developer's real credentials.\n */\nexport const credentialsPath = (dir: string): string =>\n\tresolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/**\n * Whether a credentials file is one the CLI created, rather than a path a profile adopted.\n *\n * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —\n * that is what makes adopting an existing directory a one-line edit — and a file we did not\n * create is not ours to remove.\n */\nexport const isInsideConfigDir = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);\n\n/**\n * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.\n *\n * {@link credentialsPath} deliberately reads an existing legacy file in place rather than\n * migrating it, so for a default config directory that file is ours even though it sits outside\n * `neon/`. Judging ownership on the current directory alone would call an install that predates\n * the rename \"adopted\".\n */\nexport const isOwnedCredentialPath = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => {\n\tif (isInsideConfigDir(configDirectory, file)) return true;\n\tif (configDirectory !== defaultDir) return false;\n\tconst legacy = legacyConfigDir();\n\treturn legacy !== undefined && isInsideConfigDir(legacy, file);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,kBAAkB;;AAG/B,MAAa,yBAAyB;;AAatC,SAAgB,UAAU,UAA6B,CAAC,GAAW;CAClE,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,UAAU,OAAO;CACrB,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,eAAe;AACpE;;;;;AAMA,SAAgB,gBACf,UAA6B,CAAC,GACT;CACrB,IAAI,YAAY,OAAO,GAAG,OAAO,KAAA;CACjC,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,sBAAsB;AAC3E;;;;;;AAkBA,SAAgB,kBACf,UACA,UAA6B,CAAC,GACT;CACrB,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,UAAU,QAAQ,KAAK,QAAQ;CACrC,IAAI,WAAW,OAAO,GACrB,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAK;CAE5D,MAAM,YAAY,gBAAgB,OAAO;CACzC,IAAI,WAAW;EACd,MAAM,SAAS,QAAQ,WAAW,QAAQ;EAC1C,IAAI,WAAW,MAAM,GACpB,OAAO;GACN,MAAM;GACN,KAAK;GACL,UAAU;GACV,QAAQ;EACT;CACF;CAEA,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAM;AAC7D;;AAGA,SAAS,WAAW,KAAgC;CACnD,MAAM,MAAM,SAAS,IAAI,eAAe;CACxC,IAAI,KAAK,OAAO;CAChB,MAAM,OAAO,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,WAAW;CAC3D,OAAO,OAAO,KAAK,MAAM,SAAS,IAAI;AACvC;AAEA,SAAS,YAAY,SAAgD;CACpE,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,OACC,SAAS,QAAQ,GAAG,KACpB,SAAS,IAAI,eAAe,KAC5B,SAAS,IAAI,kBAAkB;AAEjC;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC;AAEA,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,aAAa,UAAU;;;;;;;;;;;;;AAcpC,MAAa,mBAAmB,QAC/B,kBAAkB,kBAAkB,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"paths.js","names":[],"sources":["../../src/_shared/paths.ts"],"sourcesContent":["/**\n * # Where the Neon CLIs keep their files on disk\n *\n **Deliberately impure.** It reads environment variables and touches the filesystem, which\n * `@neon/config` — the package this used to be a subpath of — must never do from its root\n * export. It lives here instead of there precisely so that a policy-facing package does not\n * carry implementor-only code.\n *\n * It exists because three separate readers each grew their own answer to \"where is the\n * config directory\", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but\n * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init\n * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote\n * credentials somewhere the other two never looked.\n *\n * ## The directory\n *\n * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,\n * each entry winning over the next:\n *\n * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.\n * 2. `NEON_CONFIG_DIR` — exact.\n * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.\n * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.\n *\n * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that\n * quietly read `~/.config/neonctl` would defeat the point of passing it.\n *\n * ## The files\n *\n * {@link resolveConfigFile} answers \"which path should I use for this file\", and it is the\n * same answer for reading and writing:\n *\n * - Present in `neon/` → use it.\n * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is\n * never copied or moved, so nothing is left behind to go stale and no other tool starts\n * reading an abandoned token.\n * - Present in neither → the new location. New files only ever appear under `neon/`.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\n/** Current directory name. New files are created here. */\nexport const CONFIG_DIR_NAME = \"neon\";\n\n/** Legacy directory name, read forever so existing installs keep working untouched. */\nexport const LEGACY_CONFIG_DIR_NAME = \"neonctl\";\n\nexport interface ConfigPathOptions {\n\t/**\n\t * An explicit directory, e.g. from a `--config-dir` flag. Used exactly as given: no\n\t * environment variables are consulted and the legacy directory is never searched.\n\t */\n\tdir?: string;\n\t/** Environment to read. Defaults to `process.env`. Injectable for tests. */\n\tenv?: NodeJS.ProcessEnv;\n}\n\n/** Where files are created. See the module docs for the precedence. */\nexport function configDir(options: ConfigPathOptions = {}): string {\n\tconst explicit = explicitDir(options);\n\tif (explicit) return explicit;\n\treturn join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);\n}\n\n/**\n * The legacy directory, or `undefined` when the location was chosen explicitly (in which\n * case there is no legacy counterpart to fall back to).\n */\nexport function legacyConfigDir(\n\toptions: ConfigPathOptions = {},\n): string | undefined {\n\tif (explicitDir(options)) return undefined;\n\treturn join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);\n}\n\nexport interface ResolvedConfigFile {\n\t/** The path to use, for both reading and writing. */\n\tpath: string;\n\t/** The directory `path` lives in. */\n\tdir: string;\n\t/** True when the file was found in the legacy `neonctl` directory. */\n\tisLegacy: boolean;\n\t/** Whether the file exists at `path` right now. */\n\texists: boolean;\n}\n\n/**\n * Resolve one file inside the config directory. Prefers the current location, falls back to\n * an existing legacy file **in place**, and otherwise points at the current location so new\n * files are created there.\n */\nexport function resolveConfigFile(\n\tfileName: string,\n\toptions: ConfigPathOptions = {},\n): ResolvedConfigFile {\n\tconst dir = configDir(options);\n\tconst current = resolve(dir, fileName);\n\tif (existsSync(current))\n\t\treturn { path: current, dir, isLegacy: false, exists: true };\n\n\tconst legacyDir = legacyConfigDir(options);\n\tif (legacyDir) {\n\t\tconst legacy = resolve(legacyDir, fileName);\n\t\tif (existsSync(legacy))\n\t\t\treturn {\n\t\t\t\tpath: legacy,\n\t\t\t\tdir: legacyDir,\n\t\t\t\tisLegacy: true,\n\t\t\t\texists: true,\n\t\t\t};\n\t}\n\n\treturn { path: current, dir, isLegacy: false, exists: false };\n}\n\n/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */\nfunction configHome(env: NodeJS.ProcessEnv): string {\n\tconst xdg = nonEmpty(env.XDG_CONFIG_HOME);\n\tif (xdg) return xdg;\n\tconst home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);\n\treturn home ? join(home, \".config\") : \".config\";\n}\n\nfunction explicitDir(options: ConfigPathOptions): string | undefined {\n\tconst env = options.env ?? process.env;\n\treturn (\n\t\tnonEmpty(options.dir) ??\n\t\tnonEmpty(env.NEON_CONFIG_DIR) ??\n\t\tnonEmpty(env.NEONCTL_CONFIG_DIR)\n\t);\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\nexport const CREDENTIALS_FILE = \"credentials.json\";\n\n/**\n * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.\n *\n * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —\n * see {@link credentialsPath} — but it is never written to, moved, or deleted.\n */\nexport const defaultDir = configDir();\n\n/**\n * Where this invocation's `credentials.json` lives.\n *\n * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`\n * directory is used **in place**: an install that predates the rename keeps working, and its\n * credentials are never duplicated into a second location where one copy could go stale while\n * another tool still reads it.\n *\n * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an\n * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a\n * scratch directory must never pick up a developer's real credentials.\n */\nexport const credentialsPath = (dir: string): string =>\n\tresolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;\n\n/**\n * Whether a credentials file is one the CLI created, rather than a path a profile adopted.\n *\n * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —\n * that is what makes adopting an existing directory a one-line edit — and a file we did not\n * create is not ours to remove.\n */\nexport const isInsideConfigDir = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);\n\n/**\n * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.\n *\n * {@link credentialsPath} deliberately reads an existing legacy file in place rather than\n * migrating it, so for a default config directory that file is ours even though it sits outside\n * `neon/`. Judging ownership on the current directory alone would call an install that predates\n * the rename \"adopted\".\n */\nexport const isOwnedCredentialPath = (\n\tconfigDirectory: string,\n\tfile: string,\n): boolean => {\n\tif (isInsideConfigDir(configDirectory, file)) return true;\n\tif (configDirectory !== defaultDir) return false;\n\tconst legacy = legacyConfigDir();\n\treturn legacy !== undefined && isInsideConfigDir(legacy, file);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,kBAAkB;;AAG/B,MAAa,yBAAyB;;AAatC,SAAgB,UAAU,UAA6B,CAAC,GAAW;CAClE,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,UAAU,OAAO;CACrB,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,eAAe;AACpE;;;;;AAMA,SAAgB,gBACf,UAA6B,CAAC,GACT;CACrB,IAAI,YAAY,OAAO,GAAG,OAAO,KAAA;CACjC,OAAO,KAAK,WAAW,QAAQ,OAAO,QAAQ,GAAG,GAAG,sBAAsB;AAC3E;;;;;;AAkBA,SAAgB,kBACf,UACA,UAA6B,CAAC,GACT;CACrB,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,UAAU,QAAQ,KAAK,QAAQ;CACrC,IAAI,WAAW,OAAO,GACrB,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAK;CAE5D,MAAM,YAAY,gBAAgB,OAAO;CACzC,IAAI,WAAW;EACd,MAAM,SAAS,QAAQ,WAAW,QAAQ;EAC1C,IAAI,WAAW,MAAM,GACpB,OAAO;GACN,MAAM;GACN,KAAK;GACL,UAAU;GACV,QAAQ;EACT;CACF;CAEA,OAAO;EAAE,MAAM;EAAS;EAAK,UAAU;EAAO,QAAQ;CAAM;AAC7D;;AAGA,SAAS,WAAW,KAAgC;CACnD,MAAM,MAAM,SAAS,IAAI,eAAe;CACxC,IAAI,KAAK,OAAO;CAChB,MAAM,OAAO,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,WAAW;CAC3D,OAAO,OAAO,KAAK,MAAM,SAAS,IAAI;AACvC;AAEA,SAAS,YAAY,SAAgD;CACpE,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,OACC,SAAS,QAAQ,GAAG,KACpB,SAAS,IAAI,eAAe,KAC5B,SAAS,IAAI,kBAAkB;AAEjC;AAEA,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC;AAEA,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,aAAa,UAAU;;;;;;;;;;;;;AAcpC,MAAa,mBAAmB,QAC/B,kBAAkB,kBAAkB,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;;;;;;;;AASxE,MAAa,qBACZ,iBACA,SACa,GAAG,QAAQ,IAAI,EAAE,GAAG,WAAW,GAAG,QAAQ,eAAe,EAAE,EAAE;;;;;;;;;AAU3E,MAAa,yBACZ,iBACA,SACa;CACb,IAAI,kBAAkB,iBAAiB,IAAI,GAAG,OAAO;CACrD,IAAI,oBAAoB,YAAY,OAAO;CAC3C,MAAM,SAAS,gBAAgB;CAC/B,OAAO,WAAW,KAAA,KAAa,kBAAkB,QAAQ,IAAI;AAC9D"}
|
|
@@ -1,7 +1,145 @@
|
|
|
1
1
|
//#region src/_shared/profiles.d.ts
|
|
2
|
-
|
|
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";
|
|
3
45
|
/** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
|
|
4
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[];
|
|
5
143
|
//#endregion
|
|
6
|
-
export { DEFAULT_PROFILE };
|
|
144
|
+
export { DEFAULT_PROFILE, PROFILES_FILE, ProfileEntry, ProfilesFile, ProfilesRead, ResolvedProfile, assertProfilesUsable, assertValidProfileName, inspectProfiles, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, removeProfileEntry, resolveProfile, selectProfileName, upsertProfile };
|
|
7
145
|
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","names":[],"sources":["../../src/_shared/profiles.ts"],"mappings":"
|
|
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
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
|
|
2
|
+
import { writeSecretFile } from "./secure_file.js";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
5
|
//#region src/_shared/profiles.ts
|
|
5
6
|
/**
|
|
6
7
|
* # Profiles — several Neon accounts in one config directory
|
|
@@ -49,6 +50,11 @@ const PROFILES_FILE = "profiles.json";
|
|
|
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._-]*$/;
|
|
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
|
+
};
|
|
52
58
|
/** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
|
|
53
59
|
const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
54
60
|
/**
|
|
@@ -94,6 +100,38 @@ const inspectProfiles = (dir) => {
|
|
|
94
100
|
}
|
|
95
101
|
};
|
|
96
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
|
+
};
|
|
97
135
|
/** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
|
|
98
136
|
const resolveProfile = (dir, name) => {
|
|
99
137
|
const read = inspectProfiles(dir);
|
|
@@ -115,10 +153,76 @@ const resolveProfile = (dir, name) => {
|
|
|
115
153
|
const known = file ? Object.keys(file.profiles).join(", ") : DEFAULT_PROFILE;
|
|
116
154
|
throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
|
|
117
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
|
+
};
|
|
118
212
|
const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
|
|
119
213
|
/** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
|
|
120
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
|
+
}
|
|
121
225
|
//#endregion
|
|
122
|
-
export { DEFAULT_PROFILE, PROFILES_FILE, inspectProfiles, profilesFilePath, resolveProfile };
|
|
226
|
+
export { DEFAULT_PROFILE, PROFILES_FILE, assertProfilesUsable, assertValidProfileName, inspectProfiles, listProfiles, newProfileCredentialsPath, onlyDefaultRemains, profilesFilePath, readProfiles, removeProfileEntry, resolveProfile, selectProfileName, upsertProfile };
|
|
123
227
|
|
|
124
228
|
//# sourceMappingURL=profiles.js.map
|
|
@@ -1 +1 @@
|
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,gBAAgB;;AAG7B,MAAa,kBAAkB;;AAG/B,MAAM,eAAe;;AAyCrB,MAAa,oBAAoB,QAChC,kBAAkB,eAAe,QAAQ,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;;;;;;;;;AAiBrE,MAAa,mBAAmB,QAA8B;CAC7D,MAAM,OAAO,iBAAiB,GAAG;CACjC,IAAI,CAAC,WAAW,IAAI,GAAG,OAAO,EAAE,MAAM,SAAS;CAC/C,MAAM,UAAU,SAA+B;EAC9C,MAAM;EACN,QAAQ,GAAG,KAAK,yCAAyC;CAC1D;CAIA,IAAI;CACJ,IAAI;EACH,WAAW,aAAa,MAAM,MAAM;CACrC,SAAS,KAAK;EACb,MAAM,OAAQ,IAA8B;EAC5C,OAAO,OACN,OAAO,0BAA0B,SAAS,mBAC3C;CACD;CACA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,QAAQ;CAC7B,QAAQ;EACP,OAAO,OAAO,sBAAsB;CACrC;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO,OAAO,+BAA+B;CAC9C,MAAM,WAAY,OAAwB;CAC1C,IACC,aAAa,QACb,OAAO,aAAa,YACpB,MAAM,QAAQ,QAAQ,GAEtB,OAAO,OAAO,6BAA6B;CAC5C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;EACrD,IAAI,CAAC,aAAa,KAAK,IAAI,GAC1B,OAAO,OAAO,IAAI,KAAK,8BAA8B;EACtD,IACC,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,MAAM,gBAAgB,YAC7B,MAAM,YAAY,KAAK,MAAM,IAE7B,OAAO,OAAO,YAAY,KAAK,8BAA8B;CAE/D;CACA,OAAO;EAAE,MAAM;EAAM,MAAM;GAAE,SAAS;GAAG;EAAS;CAAE;AACrD;;AA6CA,MAAa,kBAAkB,KAAa,SAAkC;CAC7E,MAAM,OAAO,gBAAgB,GAAG;CAGhC,IAAI,KAAK,SAAS,cAAc,SAAA,WAC/B,MAAM,IAAI,MACT,GAAG,KAAK,OAAO,iEAChB;CAED,MAAM,OAAO,KAAK,SAAS,OAAO,KAAK,OAAO;CAC9C,MAAM,QAAQ,MAAM,SAAS;CAE7B,IAAI,OACH,OAAO;EACN;EACA,iBAAiB,iBAAiB,KAAK,MAAM,WAAW;EACxD,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;EAC5C,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C,UAAU;CACX;CAKD,IAAI,SAAA,WACH,OAAO;EACN;EACA,iBAAiB,gBAAgB,GAAG;EACpC,UAAU;CACX;CAGD,MAAM,QAAQ,OACX,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,IACpC;CACH,MAAM,IAAI,MACT,oBAAoB,KAAK,qBAAqB,MAAM,yCAAyC,KAAK,IACnG;AACD;AAmGA,MAAM,oBAAoB,KAAa,UACtC,WAAW,KAAK,IAAI,QAAQ,QAAQ,YAAY,GAAG,GAAG,KAAK;;AAG5D,MAAM,eAAe,QACpB,QAAQ,iBAAiB,GAAG,GAAG,IAAI"}
|
|
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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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure_file.d.ts","names":[],"sources":["../../src/_shared/secure_file.ts"],"mappings":";;AAIa,cAAA,gBAAA,GAAgB,GAAA;AAoB7B;;;;;;;;;;;;;;;;;;cAAa"}
|