@neondatabase/env 0.15.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.
Files changed (48) hide show
  1. package/README.md +21 -29
  2. package/dist/cli.js +971 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{lib/env.js → env.js} +70 -219
  5. package/dist/env.js.map +1 -0
  6. package/dist/index.d.ts +528 -2
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +197 -1
  9. package/dist/index.js.map +1 -0
  10. package/package.json +11 -14
  11. package/dist/_shared/auth_selection.js +0 -76
  12. package/dist/_shared/auth_selection.js.map +0 -1
  13. package/dist/_shared/credentials.js +0 -131
  14. package/dist/_shared/credentials.js.map +0 -1
  15. package/dist/_shared/paths.js +0 -131
  16. package/dist/_shared/paths.js.map +0 -1
  17. package/dist/_shared/profiles.d.ts +0 -7
  18. package/dist/_shared/profiles.d.ts.map +0 -1
  19. package/dist/_shared/profiles.js +0 -124
  20. package/dist/_shared/profiles.js.map +0 -1
  21. package/dist/config/dist/lib/define-config.d.ts +0 -20
  22. package/dist/config/dist/lib/define-config.d.ts.map +0 -1
  23. package/dist/config/dist/lib/neon-api.d.ts +0 -375
  24. package/dist/config/dist/lib/neon-api.d.ts.map +0 -1
  25. package/dist/config/dist/lib/types.d.ts +0 -544
  26. package/dist/config/dist/lib/types.d.ts.map +0 -1
  27. package/dist/config/dist/v1.d.ts +0 -5
  28. package/dist/lib/cli/commands.d.ts +0 -68
  29. package/dist/lib/cli/commands.d.ts.map +0 -1
  30. package/dist/lib/cli/commands.js +0 -233
  31. package/dist/lib/cli/commands.js.map +0 -1
  32. package/dist/lib/cli/resolve-api-key.d.ts +0 -29
  33. package/dist/lib/cli/resolve-api-key.d.ts.map +0 -1
  34. package/dist/lib/cli/resolve-api-key.js +0 -74
  35. package/dist/lib/cli/resolve-api-key.js.map +0 -1
  36. package/dist/lib/cli/resolve-context.d.ts +0 -34
  37. package/dist/lib/cli/resolve-context.d.ts.map +0 -1
  38. package/dist/lib/cli/resolve-context.js +0 -88
  39. package/dist/lib/cli/resolve-context.js.map +0 -1
  40. package/dist/lib/env.d.ts +0 -509
  41. package/dist/lib/env.d.ts.map +0 -1
  42. package/dist/lib/env.js.map +0 -1
  43. package/dist/lib/reuse-secrets.d.ts +0 -95
  44. package/dist/lib/reuse-secrets.d.ts.map +0 -1
  45. package/dist/lib/reuse-secrets.js +0 -181
  46. package/dist/lib/reuse-secrets.js.map +0 -1
  47. package/dist/runtime.d.ts +0 -2
  48. package/dist/runtime.js +0 -2
@@ -1 +0,0 @@
1
- {"version":3,"file":"credentials.js","names":[],"sources":["../../src/_shared/credentials.ts"],"sourcesContent":["/**\n * # Stored credentials — one file per account, two kinds\n *\n * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says\n * what kind of credential it holds. Adding API-key support this way rather than adding a\n * second pointer to `profiles.json` keeps a profile what it already was — one name, one path\n * — and means `profiles.json` needs no schema change at all.\n *\n * ```json\n * // oauth: every file written before this existed. An absent `type` means this.\n * { \"access_token\": \"…\", \"refresh_token\": \"…\", \"expires_at\": 1786…, \"user_id\": \"…\" }\n *\n * // api_key, stored by `neon profile create --api-key`\n * { \"type\": \"api_key\", \"api_key\": \"napi_…\", \"user_id\": \"…\" }\n *\n * // api_key minted by `--mint --org-id`, which records the scope it was issued at\n * { \"type\": \"api_key\", \"api_key\": \"napi_…\", \"key_id\": 123, \"org_id\": \"org-…\" }\n * ```\n *\n * ## One profile, one kind\n *\n * A credentials file holds an API key or an OAuth session, never both, and `type` states\n * which. An earlier draft let the two coexist — the idea being that a key could keep the\n * session it was minted from and so rotate without a browser. It did not survive review, for\n * two reasons that are worth recording so nobody rebuilds it:\n *\n * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key\n * failed to mint and never fell back to the session sitting beside it.\n * 2. **It could mix accounts.** Nothing compared the identity of the credential being written\n * with the one already there, so a profile could hold one account's session and another's\n * key, told apart only by a single string. Flip or lose `type` and the profile silently\n * becomes a different person.\n *\n * Recovery from a dead key is therefore one browser login — `neon profile create <name>\n * --mint --force` — which is what the retained session was supposed to save and never did.\n *\n * ## Older releases\n *\n * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and\n * looks for `access_token`. An `api_key` profile has none, so an older release falls through\n * to its browser login rather than crashing. That it does not crash is why `credentials`\n * stays a required pointer: an entry without one makes 2.41 and 2.42 throw\n * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { writeSecretFile } from \"./secure_file.js\";\n\nexport const OAUTH = \"oauth\";\nexport const API_KEY = \"api_key\";\n\nexport type CredentialKind = typeof OAUTH | typeof API_KEY;\n\n/**\n * The on-disk shape. Every field is optional because the two kinds overlap and because an\n * OAuth token endpoint response carries more than we name here — the index signature keeps\n * those extra fields on a round-trip rather than dropping them.\n */\nexport type StoredCredentials = {\n\ttype?: string;\n\tapi_key?: string;\n\tkey_id?: number;\n\t/** Set when the key was minted for an organization rather than the account. */\n\torg_id?: string;\n\t/** Set when the key was narrowed to a single project. Implies `org_id`. */\n\tproject_id?: string;\n\tuser_id?: string;\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_at?: number;\n\t[key: string]: unknown;\n};\n\n/**\n * Where a credential lives, and which profile points at it.\n *\n * Both halves are needed to report a broken file: the path says which file to open, and the\n * profile is what every recovery command takes as its argument. Carrying only the path is what\n * produced errors telling the user to run `neon profile create <name> --force` with the\n * placeholder intact — a command an agent will run verbatim and be told `Invalid profile name\n * \"<name>\"`.\n */\nexport type CredentialLocation = {\n\tpath: string;\n\tprofile: string;\n};\n\n/**\n * Which credential in this file authenticates, by declaration alone.\n *\n * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot\n * interpret is a misconfiguration the user has to see: treating it as OAuth would send them\n * to a browser login that silently replaces a credential they meant to keep, and treating it\n * as an API key would authenticate with whatever `api_key` happened to be there.\n *\n * This deliberately does not check that an `api_key` file has a key — `neon profile list`\n * needs the kind of a file it is not about to authenticate with, and must be able to report a\n * broken one rather than throwing halfway through a table.\n */\nexport const credentialKind = (\n\tcredentials: StoredCredentials,\n\tat: CredentialLocation,\n): CredentialKind => {\n\tconst declared = credentials.type;\n\tif (declared === undefined || declared === OAUTH) return OAUTH;\n\tif (declared === API_KEY) return API_KEY;\n\t// The value is not quoted back. Everything in this file is secret material, and a\n\t// corrupted or hand-edited file can put a key anywhere in it — including here. Naming the\n\t// file is enough to act on, and it cannot leak what the file holds.\n\tthrow new Error(\n\t\t`${at.path} declares a \"type\" this version does not understand. Expected \"${OAUTH}\" or \"${API_KEY}\". ${repair(at)}`,\n\t);\n};\n\n/**\n * The way out of a credentials file that cannot be read.\n *\n * One sentence, shared by every such error, because they all have the same two answers: write\n * a new credential over it, or delete it and start again.\n */\nconst repair = (at: CredentialLocation): string =>\n\t`Replace it deliberately with \\`neon profile create ${at.profile} --force\\`, or delete the file.`;\n\n/** A credentials file resolved far enough to authenticate with. */\nexport type InterpretedCredentials =\n\t| { kind: typeof API_KEY; apiKey: string }\n\t| { kind: typeof OAUTH };\n\n/**\n * Resolve what to authenticate with, validating that the declared kind is actually usable.\n *\n * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user\n * asked for a key, and quietly opening a browser instead would replace the credential they\n * were trying to fix.\n */\nexport const interpretCredentials = (\n\tcredentials: StoredCredentials,\n\tat: CredentialLocation,\n): InterpretedCredentials => {\n\tif (credentialKind(credentials, at) === OAUTH) return { kind: OAUTH };\n\tconst apiKey = nonEmpty(credentials.api_key);\n\tif (apiKey === undefined) {\n\t\tthrow new Error(\n\t\t\t`${at.path} declares \"type\": \"${API_KEY}\" but has no \"api_key\" value. ${repair(at)}`,\n\t\t);\n\t}\n\treturn { kind: API_KEY, apiKey };\n};\n\n/**\n * Read a credentials file, or `null` when there is nothing usable there.\n *\n * Missing and unparseable both return `null`, because both are recoverable by authenticating\n * again and the callers already treat \"no credentials\" as \"log in\". A file that parses but\n * contradicts itself is different — {@link credentialKind} throws for that, since re-running\n * `auth` would paper over a mistake rather than fix it.\n */\nexport type CredentialsRead =\n\t| { kind: \"ok\"; credentials: StoredCredentials }\n\t| { kind: \"absent\" }\n\t/** The file is there but cannot be understood. `reason` is safe to print. */\n\t| { kind: \"unusable\"; reason: string };\n\n/**\n * Read and classify a credentials file, without deciding what to do about it.\n *\n * A permission or I/O error still throws: there may be a perfectly good credential here that\n * we cannot see, and treating that as absent would send the user to a browser login that\n * overwrites it.\n */\nexport const inspectCredentials = (path: string): CredentialsRead => {\n\tlet contents: string;\n\ttry {\n\t\tcontents = readFileSync(path, \"utf8\");\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code === \"ENOENT\")\n\t\t\treturn { kind: \"absent\" };\n\t\tthrow err;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(contents);\n\t} catch {\n\t\t// The parser's message is deliberately discarded. V8 quotes a window of the input\n\t\t// around the syntax error — on Node 24 a truncated credentials file produced\n\t\t// `Unexpected token 'a', ...\"api_key\":napi_SUPERS\"... is not valid JSON` — and this\n\t\t// reason is printed by `profile list` and by every failed authentication. A malformed\n\t\t// secret file is exactly when a diagnostic must say less, not more.\n\t\treturn {\n\t\t\tkind: \"unusable\",\n\t\t\treason: `${path} is not valid JSON, so the credential in it cannot be read`,\n\t\t};\n\t}\n\tif (\n\t\tparsed === null ||\n\t\ttypeof parsed !== \"object\" ||\n\t\tArray.isArray(parsed)\n\t) {\n\t\treturn {\n\t\t\tkind: \"unusable\",\n\t\t\treason: `${path} does not contain a credentials object`,\n\t\t};\n\t}\n\treturn { kind: \"ok\", credentials: parsed as StoredCredentials };\n};\n\n/**\n * The credential at `path`, or `null` when the file is not there.\n *\n * A damaged file is an error, not an absence. Treating it as absent — which is what this used to\n * do — meant any read-only command could repair it by starting a browser sign-in and overwriting\n * it, **possibly as a different account**, with the user never having asked for a repair and no\n * way back to whatever was in the file. Failing here costs one deliberate command; the message\n * names it.\n *\n * `profile list` and telemetry use {@link inspectCredentials} instead, because describing a\n * broken credential is not the same as using one.\n */\nexport const readCredentials = (\n\tat: CredentialLocation,\n): StoredCredentials | null => {\n\tconst read = inspectCredentials(at.path);\n\tif (read.kind === \"unusable\") {\n\t\tthrow new Error(`${read.reason}. ${repair(at)}`);\n\t}\n\treturn read.kind === \"ok\" ? read.credentials : null;\n};\n\nexport const writeCredentials = (\n\tpath: string,\n\tcredentials: StoredCredentials,\n): void => {\n\twriteSecretFile(path, JSON.stringify(credentials));\n};\n\n/** The scope a minted key was issued at. Absent org means an account key. */\nexport type KeyScope = {\n\torgId?: string;\n\tprojectId?: string;\n};\n\n/**\n * Build an `api_key` credentials object. Nothing from a previous credential is carried over.\n *\n * The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint\n * the replacement on the same endpoint, and an org or project key minted as an account key\n * would silently widen what the profile reaches.\n */\nexport const apiKeyCredentials = ({\n\tapiKey,\n\tkeyId,\n\tuserId,\n\tscope,\n}: {\n\tapiKey: string;\n\tkeyId?: number;\n\tuserId?: string;\n\tscope?: KeyScope;\n}): StoredCredentials => ({\n\ttype: API_KEY,\n\tapi_key: apiKey,\n\t...(keyId !== undefined ? { key_id: keyId } : {}),\n\t...(userId !== undefined ? { user_id: userId } : {}),\n\t...(scope?.orgId !== undefined ? { org_id: scope.orgId } : {}),\n\t...(scope?.projectId !== undefined ? { project_id: scope.projectId } : {}),\n});\n\n/** The scope recorded on a stored credential. */\nexport const scopeOf = (credentials: StoredCredentials): KeyScope => ({\n\t...(typeof credentials.org_id === \"string\"\n\t\t? { orgId: credentials.org_id }\n\t\t: {}),\n\t...(typeof credentials.project_id === \"string\"\n\t\t? { projectId: credentials.project_id }\n\t\t: {}),\n});\n\n/** How to describe a scope in output. */\nexport const describeScope = (scope: KeyScope): string => {\n\tif (scope.projectId !== undefined) return `project ${scope.projectId}`;\n\tif (scope.orgId !== undefined) return `org ${scope.orgId}`;\n\treturn \"account\";\n};\n\nfunction nonEmpty(value: unknown): string | undefined {\n\tif (typeof value !== \"string\") return undefined;\n\tconst trimmed = value.trim();\n\treturn trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Whether a stored credential is the same secret as the one about to replace it.\n *\n * Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it\n * would revoke the credential the command has just committed to. Trimmed on both sides, because\n * a key read from a file or a pipe arrives with a trailing newline.\n */\nexport const isSameCredential = (\n\texistingKey: string | undefined,\n\treplacementKey: string | undefined,\n): boolean => {\n\tif (existingKey === undefined || replacementKey === undefined) return false;\n\tconst trimmed = existingKey.trim();\n\treturn trimmed !== \"\" && trimmed === replacementKey.trim();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,QAAQ;AACrB,MAAa,UAAU;;;;;;;;;;;;;AAkDvB,MAAa,kBACZ,aACA,OACoB;CACpB,MAAM,WAAW,YAAY;CAC7B,IAAI,aAAa,KAAA,KAAa,aAAA,SAAoB,OAAO;CACzD,IAAI,aAAA,WAAsB,OAAO;CAIjC,MAAM,IAAI,MACT,GAAG,GAAG,KAAK,iEAAiE,MAAM,QAAQ,QAAQ,KAAK,OAAO,EAAE,GACjH;AACD;;;;;;;AAQA,MAAM,UAAU,OACf,sDAAsD,GAAG,QAAQ;;;;;;;;AAclE,MAAa,wBACZ,aACA,OAC4B;CAC5B,IAAI,eAAe,aAAa,EAAE,MAAA,SAAa,OAAO,EAAE,MAAM,MAAM;CACpE,MAAM,SAAS,SAAS,YAAY,OAAO;CAC3C,IAAI,WAAW,KAAA,GACd,MAAM,IAAI,MACT,GAAG,GAAG,KAAK,qBAAqB,QAAQ,gCAAgC,OAAO,EAAE,GAClF;CAED,OAAO;EAAE,MAAM;EAAS;CAAO;AAChC;;;;;;;;AAuBA,MAAa,sBAAsB,SAAkC;CACpE,IAAI;CACJ,IAAI;EACH,WAAW,aAAa,MAAM,MAAM;CACrC,SAAS,KAAK;EACb,IAAK,IAA8B,SAAS,UAC3C,OAAO,EAAE,MAAM,SAAS;EACzB,MAAM;CACP;CAEA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,QAAQ;CAC7B,QAAQ;EAMP,OAAO;GACN,MAAM;GACN,QAAQ,GAAG,KAAK;EACjB;CACD;CACA,IACC,WAAW,QACX,OAAO,WAAW,YAClB,MAAM,QAAQ,MAAM,GAEpB,OAAO;EACN,MAAM;EACN,QAAQ,GAAG,KAAK;CACjB;CAED,OAAO;EAAE,MAAM;EAAM,aAAa;CAA4B;AAC/D;AAgFA,SAAS,SAAS,OAAoC;CACrD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}
@@ -1,131 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- //#region src/_shared/paths.ts
4
- /**
5
- * # Where the Neon CLIs keep their files on disk
6
- *
7
- **Deliberately impure.** It reads environment variables and touches the filesystem, which
8
- * `@neon/config` — the package this used to be a subpath of — must never do from its root
9
- * export. It lives here instead of there precisely so that a policy-facing package does not
10
- * carry implementor-only code.
11
- *
12
- * It exists because three separate readers each grew their own answer to "where is the
13
- * config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
14
- * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
15
- * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
16
- * credentials somewhere the other two never looked.
17
- *
18
- * ## The directory
19
- *
20
- * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
21
- * each entry winning over the next:
22
- *
23
- * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
24
- * 2. `NEON_CONFIG_DIR` — exact.
25
- * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
26
- * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
27
- *
28
- * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
29
- * quietly read `~/.config/neonctl` would defeat the point of passing it.
30
- *
31
- * ## The files
32
- *
33
- * {@link resolveConfigFile} answers "which path should I use for this file", and it is the
34
- * same answer for reading and writing:
35
- *
36
- * - Present in `neon/` → use it.
37
- * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
38
- * never copied or moved, so nothing is left behind to go stale and no other tool starts
39
- * reading an abandoned token.
40
- * - Present in neither → the new location. New files only ever appear under `neon/`.
41
- */
42
- /** Current directory name. New files are created here. */
43
- const CONFIG_DIR_NAME = "neon";
44
- /** Legacy directory name, read forever so existing installs keep working untouched. */
45
- const LEGACY_CONFIG_DIR_NAME = "neonctl";
46
- /** Where files are created. See the module docs for the precedence. */
47
- function configDir(options = {}) {
48
- const explicit = explicitDir(options);
49
- if (explicit) return explicit;
50
- return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
51
- }
52
- /**
53
- * The legacy directory, or `undefined` when the location was chosen explicitly (in which
54
- * case there is no legacy counterpart to fall back to).
55
- */
56
- function legacyConfigDir(options = {}) {
57
- if (explicitDir(options)) return void 0;
58
- return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
59
- }
60
- /**
61
- * Resolve one file inside the config directory. Prefers the current location, falls back to
62
- * an existing legacy file **in place**, and otherwise points at the current location so new
63
- * files are created there.
64
- */
65
- function resolveConfigFile(fileName, options = {}) {
66
- const dir = configDir(options);
67
- const current = resolve(dir, fileName);
68
- if (existsSync(current)) return {
69
- path: current,
70
- dir,
71
- isLegacy: false,
72
- exists: true
73
- };
74
- const legacyDir = legacyConfigDir(options);
75
- if (legacyDir) {
76
- const legacy = resolve(legacyDir, fileName);
77
- if (existsSync(legacy)) return {
78
- path: legacy,
79
- dir: legacyDir,
80
- isLegacy: true,
81
- exists: true
82
- };
83
- }
84
- return {
85
- path: current,
86
- dir,
87
- isLegacy: false,
88
- exists: false
89
- };
90
- }
91
- /** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
92
- function configHome(env) {
93
- const xdg = nonEmpty(env.XDG_CONFIG_HOME);
94
- if (xdg) return xdg;
95
- const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
96
- return home ? join(home, ".config") : ".config";
97
- }
98
- function explicitDir(options) {
99
- const env = options.env ?? process.env;
100
- return nonEmpty(options.dir) ?? nonEmpty(env.NEON_CONFIG_DIR) ?? nonEmpty(env.NEONCTL_CONFIG_DIR);
101
- }
102
- function nonEmpty(value) {
103
- if (typeof value !== "string") return void 0;
104
- const trimmed = value.trim();
105
- return trimmed === "" ? void 0 : trimmed;
106
- }
107
- const CREDENTIALS_FILE = "credentials.json";
108
- /**
109
- * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
110
- *
111
- * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
112
- * see {@link credentialsPath} — but it is never written to, moved, or deleted.
113
- */
114
- const defaultDir = configDir();
115
- /**
116
- * Where this invocation's `credentials.json` lives.
117
- *
118
- * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
119
- * directory is used **in place**: an install that predates the rename keeps working, and its
120
- * credentials are never duplicated into a second location where one copy could go stale while
121
- * another tool still reads it.
122
- *
123
- * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
124
- * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
125
- * scratch directory must never pick up a developer's real credentials.
126
- */
127
- const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
128
- //#endregion
129
- export { CONFIG_DIR_NAME, CREDENTIALS_FILE, LEGACY_CONFIG_DIR_NAME, configDir, credentialsPath, defaultDir, legacyConfigDir, resolveConfigFile };
130
-
131
- //# sourceMappingURL=paths.js.map
@@ -1 +0,0 @@
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,7 +0,0 @@
1
- //#region src/_shared/profiles.d.ts
2
-
3
- /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
4
- declare const DEFAULT_PROFILE = "DEFAULT";
5
- //#endregion
6
- export { DEFAULT_PROFILE };
7
- //# sourceMappingURL=profiles.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"profiles.d.ts","names":[],"sources":["../../src/_shared/profiles.ts"],"mappings":";;;cAmDa,eAAA"}
@@ -1,124 +0,0 @@
1
- import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { isAbsolute, resolve } from "node:path";
4
- //#region src/_shared/profiles.ts
5
- /**
6
- * # Profiles — several Neon accounts in one config directory
7
- *
8
- * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
9
- * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
10
- * "active profile" state to fall out of sync, and no migration.
11
- *
12
- * ```
13
- * ~/.config/neon/
14
- * ├── credentials.json # this IS the DEFAULT profile, not a copy of it
15
- * ├── credentials.work.json # created by `neon auth --profile work`
16
- * └── profiles.json # created only once a second profile exists
17
- * ```
18
- *
19
- * `profiles.json` maps a name to a path, and the path may point anywhere — which is what
20
- * makes adopting an existing directory a one-line edit rather than an import command:
21
- *
22
- * ```json
23
- * {
24
- * "version": 1,
25
- * "profiles": {
26
- * "DEFAULT": { "credentials": "credentials.json" },
27
- * "work": {
28
- * "credentials": "../neonctl-databricks/credentials.json",
29
- * "label": "someone@example.com"
30
- * }
31
- * }
32
- * }
33
- * ```
34
- *
35
- * ## Selection
36
- *
37
- * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
38
- * `profile use` command, so nothing persists that could disagree with what you typed.
39
- *
40
- * ## Compatibility
41
- *
42
- * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
43
- * resolves to `credentials.json` in the config directory (including an existing one in the
44
- * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
45
- * profile is, and nothing is ever moved.
46
- */
47
- const PROFILES_FILE = "profiles.json";
48
- /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
49
- const DEFAULT_PROFILE = "DEFAULT";
50
- /** Profile names become part of a filename, so keep them boring. */
51
- const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
52
- /** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
53
- const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
54
- /**
55
- * Read and classify `profiles.json` without deciding what to do about it.
56
- *
57
- * Entry keys and shapes are validated here rather than at each use. A key is a profile name,
58
- * and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
59
- * it would travel into error messages as a recovery command nobody can run, and into a
60
- * `credentials.<name>.json` filename.
61
- */
62
- const inspectProfiles = (dir) => {
63
- const path = profilesFilePath(dir);
64
- if (!existsSync(path)) return { kind: "absent" };
65
- const broken = (why) => ({
66
- kind: "unusable",
67
- reason: `${path} could not be read as a profiles file: ${why}`
68
- });
69
- let contents;
70
- try {
71
- contents = readFileSync(path, "utf8");
72
- } catch (err) {
73
- const code = err.code;
74
- return broken(code ? `reading it failed with ${code}` : "reading it failed");
75
- }
76
- let parsed;
77
- try {
78
- parsed = JSON.parse(contents);
79
- } catch {
80
- return broken("it is not valid JSON");
81
- }
82
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return broken("it does not contain an object");
83
- const profiles = parsed.profiles;
84
- if (profiles === null || typeof profiles !== "object" || Array.isArray(profiles)) return broken("it has no `profiles` object");
85
- for (const [name, entry] of Object.entries(profiles)) {
86
- if (!NAME_PATTERN.test(name)) return broken(`"${name}" is not a valid profile name`);
87
- if (entry === null || typeof entry !== "object" || typeof entry.credentials !== "string" || entry.credentials.trim() === "") return broken(`profile "${name}" has no \`credentials\` path`);
88
- }
89
- return {
90
- kind: "ok",
91
- file: {
92
- version: 1,
93
- profiles
94
- }
95
- };
96
- };
97
- /** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
98
- const resolveProfile = (dir, name) => {
99
- const read = inspectProfiles(dir);
100
- if (read.kind === "unusable" && name !== "DEFAULT") throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
101
- const file = read.kind === "ok" ? read.file : null;
102
- const entry = file?.profiles[name];
103
- if (entry) return {
104
- name,
105
- credentialsPath: resolveEntryPath(dir, entry.credentials),
106
- ...entry.label ? { label: entry.label } : {},
107
- ...entry.userId ? { userId: entry.userId } : {},
108
- declared: true
109
- };
110
- if (name === "DEFAULT") return {
111
- name,
112
- credentialsPath: credentialsPath(dir),
113
- declared: false
114
- };
115
- const known = file ? Object.keys(file.profiles).join(", ") : DEFAULT_PROFILE;
116
- throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
117
- };
118
- const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
119
- /** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
120
- const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
121
- //#endregion
122
- export { DEFAULT_PROFILE, PROFILES_FILE, inspectProfiles, profilesFilePath, resolveProfile };
123
-
124
- //# 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,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"}