@neondatabase/env 0.14.1 → 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 -26
- 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 +23 -4
- package/dist/_shared/env-core/reuse-secrets.d.ts.map +1 -0
- package/dist/{lib → _shared/env-core}/reuse-secrets.js +10 -7
- 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
package/README.md
CHANGED
|
@@ -12,14 +12,13 @@ npm install @neon/env
|
|
|
12
12
|
|
|
13
13
|
> **Requirements:** Node.js >= 20.19.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## What's in it
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| `@neon/env/runtime` | The stateful half: `fetchEnvReusingSecrets`, for tools that resolve the same branch repeatedly and must not re-mint a credential each time. It reads an env source and can mint and revoke credentials. Import it from a CLI or CI, never from an app bootstrap. |
|
|
17
|
+
Everything is on `@neon/env`, and none of it has side effects: `fetchEnv` asks the Neon API for a branch's env, `parseEnv` validates what was already injected into `process.env`, `toEntries` projects a resolved env into `{ KEY: value }`. Nothing here writes a file, mutates `process.env`, or creates or destroys anything on your Neon project — so importing this package from an app, a build script, or a `neon.ts` policy can't surprise you.
|
|
18
|
+
|
|
19
|
+
> **`@neon/env/runtime` was removed in 0.16.0.** It held `fetchEnvReusingSecrets`, which reads an env source and can mint and revoke branch credentials — implementation shared with the `neon` CLI, not something to hand an application. If you were importing it, the [`neon` CLI](../cli) (`neon env pull`, `neon dev`) does the same job; if you need to do it yourself, [The branch credential](#the-branch-credential) says what the hard part actually is.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Contributing? See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
|
23
22
|
|
|
24
23
|
## Functions
|
|
25
24
|
|
|
@@ -55,7 +54,6 @@ Both return the same namespaced `NeonEnv` shape: `postgres` is always present; `
|
|
|
55
54
|
| Function | Description |
|
|
56
55
|
| --- | --- |
|
|
57
56
|
| `fetchEnv(config, { projectId, branch, ... })` | Async. Calls the Neon API for the given project + branch and returns live connection strings (and Auth/Data API values when enabled). `projectId` and `branch` are required; `branch` accepts a branch **name** (e.g. `main`) or a `br-…` id. (The legacy id-only `branchId` option still works.) Pass `keys` to fetch only some vars — see [Fetching a subset](#fetching-a-subset). Reads nothing from `process.env` or disk. |
|
|
58
|
-
| `fetchEnvReusingSecrets(config, { projectId, branch, env })` | Async, from **`@neon/env/runtime`**. `fetchEnv` plus reuse of one-time secrets you already hold: verifies them against the branch, keeps what's valid, mints and revokes only when it must. Returns `{ vars, credential }`. Use this rather than `fetchEnv` anywhere the same branch is resolved repeatedly — see [The branch credential](#the-branch-credential). |
|
|
59
57
|
| `parseEnv(config)` / `parseEnv(config, slug)` / `parseEnv(config, keys)` | Sync. Reads/validates the Neon env vars already present in `process.env` against the static policy toggles. With a function `slug`, also returns a typed `function` namespace of that function's declared env keys. With a `keys` array (e.g. `["DATABASE_URL"]`), only those vars are required and returned, as a narrowed namespaced shape — the keys are typesafe against the policy. Throws `PlatformError(EnvNotInjected)` listing missing vars when the env isn't populated. |
|
|
60
58
|
| `toEntries(env)` | Project a resolved `NeonEnv` into `{ KEY: value }` pairs for cross-process transport (named after the web `.entries()` convention; returns a `Record`). |
|
|
61
59
|
|
|
@@ -135,27 +133,11 @@ These are the OS-level vars `fetchEnv` / `parseEnv` read and `toEntries` (so `ne
|
|
|
135
133
|
|
|
136
134
|
Object storage and the AI Gateway are backed by one branch credential, and the Neon API returns its secrets (`s3_secret_access_key`, `api_token`) **once**, at mint time — they aren't stored server-side, and the list endpoint returns metadata only. So there is nothing to *fetch*: `fetchEnv` mints. Call it on every `neon dev` start and you leave a live credential behind each time.
|
|
137
135
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
import { fetchEnvReusingSecrets } from "@neon/env/runtime";
|
|
142
|
-
|
|
143
|
-
const { vars, credential } = await fetchEnvReusingSecrets(config, {
|
|
144
|
-
projectId,
|
|
145
|
-
branch: "main",
|
|
146
|
-
env: { ...process.env, ...readEnvFile(".env") },
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// vars: { DATABASE_URL: "…", AWS_ACCESS_KEY_ID: "…", … } — ready to write or inject
|
|
150
|
-
if (credential.issued) {
|
|
151
|
-
console.log(`new values for ${credential.keys.join(", ")}`);
|
|
152
|
-
// credential.revoked holds the token ids it superseded
|
|
153
|
-
}
|
|
154
|
-
```
|
|
136
|
+
Handling that is the caller's problem, and it is not just "cache the secret": a persisted secret is only reusable if it still names a live credential on that branch — unrevoked, unexpired, and carrying every scope the policy needs. A presence check cannot tell a real secret from a `.env.example` placeholder.
|
|
155
137
|
|
|
156
|
-
|
|
138
|
+
No local bookkeeping is needed to do it, because the secrets carry their own credential id: `AWS_ACCESS_KEY_ID` **is** the credential's token id, and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`. So the `.env` you are about to rewrite already records which credential issued it.
|
|
157
139
|
|
|
158
|
-
|
|
140
|
+
The [`neon` CLI](../cli) does all of this — `neon env pull` and `neon dev` reuse a branch credential rather than issuing one per run. If you are calling `fetchEnv` on a loop yourself, `credentialScopesSatisfied` and `deriveCredentialScopes` from `@neon/config/v1`, plus `listCredentials` / `createCredential` / `revokeCredential` on a `NeonApi`, are the pieces you need.
|
|
159
141
|
|
|
160
142
|
### Fetching a subset
|
|
161
143
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/_shared/auth_selection.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* # Which credential an invocation authenticates with
|
|
4
|
+
*
|
|
5
|
+
* Four inputs can each answer "who am I": the `--api-key` flag, `NEON_API_KEY`, the
|
|
6
|
+
* `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so
|
|
7
|
+
* the decision can be tested without a filesystem, a network, or a config directory.
|
|
8
|
+
*
|
|
9
|
+
* ## The rule
|
|
10
|
+
*
|
|
11
|
+
* **An explicit flag beats an ambient environment variable.** That single rule fixes the bug
|
|
12
|
+
* this module exists for: before it, any API key — including one merely exported into the
|
|
13
|
+
* shell — silently voided `--profile`, so `neon --profile work …` would quietly run as
|
|
14
|
+
* whoever `NEON_API_KEY` belonged to and say nothing about it.
|
|
15
|
+
*
|
|
16
|
+
* | Given | What runs |
|
|
17
|
+
* | --- | --- |
|
|
18
|
+
* | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |
|
|
19
|
+
* | `--api-key` and `NEON_PROFILE` | the flag's key |
|
|
20
|
+
* | `--profile` and `NEON_API_KEY` | the profile |
|
|
21
|
+
* | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |
|
|
22
|
+
* | `--profile` or `NEON_PROFILE` alone | that profile |
|
|
23
|
+
* | nothing | `DEFAULT` |
|
|
24
|
+
*
|
|
25
|
+
* Two explicit flags throw rather than picking a winner. They express different intents —
|
|
26
|
+
* `--api-key` supplies a credential, `--profile` selects a stored one — so there is no
|
|
27
|
+
* reading of the command that makes both true, and guessing is how the original bug behaved.
|
|
28
|
+
*
|
|
29
|
+
* When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline
|
|
30
|
+
* that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into
|
|
31
|
+
* the environment. It warns instead of staying silent, because a disregarded account
|
|
32
|
+
* selection is precisely what nobody noticed last time.
|
|
33
|
+
*
|
|
34
|
+
* `auth` and the `profile` subcommands do not use any of this. They read the same flags with
|
|
35
|
+
* different meanings — `neon auth --profile work` names where to *write* a credential, and
|
|
36
|
+
* `neon profile create work --api-key …` names one to *store* — so their callers skip
|
|
37
|
+
* selection entirely rather than passing exemptions down here.
|
|
38
|
+
*/
|
|
39
|
+
type CredentialSelection = /** `--api-key`. Used as given; no profile is consulted and no stored file is touched. */
|
|
40
|
+
{
|
|
41
|
+
source: "explicit-api-key";
|
|
42
|
+
apiKey: string;
|
|
43
|
+
}
|
|
44
|
+
/** `NEON_API_KEY`, with the profile it displaced when there was one. */ | {
|
|
45
|
+
source: "ambient-api-key";
|
|
46
|
+
apiKey: string;
|
|
47
|
+
ignoredProfile?: string;
|
|
48
|
+
}
|
|
49
|
+
/** A profile, whose file decides whether that means an API key or OAuth. */ | {
|
|
50
|
+
source: "profile";
|
|
51
|
+
profile: string;
|
|
52
|
+
explicit: boolean;
|
|
53
|
+
};
|
|
54
|
+
type SelectionInput = {
|
|
55
|
+
/** The `--api-key` flag, before any environment fallback has been folded into it. */
|
|
56
|
+
apiKeyFlag?: string;
|
|
57
|
+
/** The `--profile` flag. */
|
|
58
|
+
profileFlag?: string;
|
|
59
|
+
/** `NEON_API_KEY`. */
|
|
60
|
+
apiKeyEnv?: string;
|
|
61
|
+
/** `NEON_PROFILE`. */
|
|
62
|
+
profileEnv?: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* What the four credential inputs were for this invocation, captured by
|
|
66
|
+
* `resolveApiKeyFromEnv` — which is the one place that reads the environment.
|
|
67
|
+
*
|
|
68
|
+
* Two reasons this is module state rather than fields on the parsed arguments, the same two
|
|
69
|
+
* that put `auth_context` here: an extra key on `args` is rejected by every command calling
|
|
70
|
+
* `.strict()`, and a hidden option to carry it would be a second undocumented way to pass a
|
|
71
|
+
* credential. One process is one invocation, so there is nothing to get out of step.
|
|
72
|
+
*
|
|
73
|
+
* Capturing the environment here rather than reading it inside {@link selectCredential} keeps
|
|
74
|
+
* the selection a function of its arguments. That is not tidiness: `ensureAuth` is called
|
|
75
|
+
* directly by tests, and reading `process.env` down in the decision made those tests depend on
|
|
76
|
+
* whether the developer running them happened to have `NEON_API_KEY` exported.
|
|
77
|
+
*/
|
|
78
|
+
type CredentialInputs = {
|
|
79
|
+
apiKeyFlag: string;
|
|
80
|
+
apiKeyEnv: string;
|
|
81
|
+
profileEnv: string;
|
|
82
|
+
};
|
|
83
|
+
declare const recordCredentialInputs: (recorded: CredentialInputs) => void;
|
|
84
|
+
declare const credentialInputs: () => CredentialInputs;
|
|
85
|
+
declare const selectCredential: ({
|
|
86
|
+
apiKeyFlag,
|
|
87
|
+
profileFlag,
|
|
88
|
+
apiKeyEnv,
|
|
89
|
+
profileEnv
|
|
90
|
+
}: SelectionInput) => CredentialSelection;
|
|
91
|
+
/** The warning for an ambient key that displaced an ambient profile, or `null`. */
|
|
92
|
+
declare const displacedProfileWarning: (selection: CredentialSelection) => string | null;
|
|
93
|
+
//#endregion
|
|
94
|
+
export { CredentialInputs, CredentialSelection, SelectionInput, credentialInputs, displacedProfileWarning, recordCredentialInputs, selectCredential };
|
|
95
|
+
//# sourceMappingURL=auth_selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth_selection.d.ts","names":[],"sources":["../../src/_shared/auth_selection.ts"],"mappings":";;AAwCA;AAQA;AAyBA;AAcA;AAIA;AAEA;AAuCC;AAvCgC;AAAA;AAAA;AAAA;AAK9B;AAAiB;AAkCnB;AAGD;;;;;;;;;;;;;;;;;;;;;;KA/FY,mBAAA;;;;;;;;;;;;;;;KAQA,cAAA;;;;;;;;;;;;;;;;;;;;;;;;KAyBA,gBAAA;;;;;cAcC,mCAAoC;cAIpC,wBAAuB;cAEvB;;;;;GAKV,mBAAiB;;cAqCP,qCACD"}
|
|
@@ -37,6 +37,15 @@ import "./profiles.js";
|
|
|
37
37
|
* `neon profile create work --api-key …` names one to *store* — so their callers skip
|
|
38
38
|
* selection entirely rather than passing exemptions down here.
|
|
39
39
|
*/
|
|
40
|
+
let inputs = {
|
|
41
|
+
apiKeyFlag: "",
|
|
42
|
+
apiKeyEnv: "",
|
|
43
|
+
profileEnv: ""
|
|
44
|
+
};
|
|
45
|
+
const recordCredentialInputs = (recorded) => {
|
|
46
|
+
inputs = recorded;
|
|
47
|
+
};
|
|
48
|
+
const credentialInputs = () => inputs;
|
|
40
49
|
const selectCredential = ({ apiKeyFlag, profileFlag, apiKeyEnv, profileEnv }) => {
|
|
41
50
|
const flagKey = nonEmpty(apiKeyFlag);
|
|
42
51
|
const flagProfile = nonEmpty(profileFlag);
|
|
@@ -71,6 +80,6 @@ function nonEmpty(value) {
|
|
|
71
80
|
return trimmed === "" ? void 0 : trimmed;
|
|
72
81
|
}
|
|
73
82
|
//#endregion
|
|
74
|
-
export { displacedProfileWarning, selectCredential };
|
|
83
|
+
export { credentialInputs, displacedProfileWarning, recordCredentialInputs, selectCredential };
|
|
75
84
|
|
|
76
85
|
//# sourceMappingURL=auth_selection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_selection.js","names":[],"sources":["../../src/_shared/auth_selection.ts"],"sourcesContent":["/**\n * # Which credential an invocation authenticates with\n *\n * Four inputs can each answer \"who am I\": the `--api-key` flag, `NEON_API_KEY`, the\n * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so\n * the decision can be tested without a filesystem, a network, or a config directory.\n *\n * ## The rule\n *\n * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug\n * this module exists for: before it, any API key — including one merely exported into the\n * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as\n * whoever `NEON_API_KEY` belonged to and say nothing about it.\n *\n * | Given | What runs |\n * | --- | --- |\n * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |\n * | `--api-key` and `NEON_PROFILE` | the flag's key |\n * | `--profile` and `NEON_API_KEY` | the profile |\n * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |\n * | `--profile` or `NEON_PROFILE` alone | that profile |\n * | nothing | `DEFAULT` |\n *\n * Two explicit flags throw rather than picking a winner. They express different intents —\n * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no\n * reading of the command that makes both true, and guessing is how the original bug behaved.\n *\n * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline\n * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into\n * the environment. It warns instead of staying silent, because a disregarded account\n * selection is precisely what nobody noticed last time.\n *\n * `auth` and the `profile` subcommands do not use any of this. They read the same flags with\n * different meanings — `neon auth --profile work` names where to *write* a credential, and\n * `neon profile create work --api-key …` names one to *store* — so their callers skip\n * selection entirely rather than passing exemptions down here.\n */\n\nimport { DEFAULT_PROFILE } from \"./profiles.js\";\n\nexport type CredentialSelection =\n\t/** `--api-key`. Used as given; no profile is consulted and no stored file is touched. */\n\t| { source: \"explicit-api-key\"; apiKey: string }\n\t/** `NEON_API_KEY`, with the profile it displaced when there was one. */\n\t| { source: \"ambient-api-key\"; apiKey: string; ignoredProfile?: string }\n\t/** A profile, whose file decides whether that means an API key or OAuth. */\n\t| { source: \"profile\"; profile: string; explicit: boolean };\n\nexport type SelectionInput = {\n\t/** The `--api-key` flag, before any environment fallback has been folded into it. */\n\tapiKeyFlag?: string;\n\t/** The `--profile` flag. */\n\tprofileFlag?: string;\n\t/** `NEON_API_KEY`. */\n\tapiKeyEnv?: string;\n\t/** `NEON_PROFILE`. */\n\tprofileEnv?: string;\n};\n\n/**\n * What the four credential inputs were for this invocation, captured by\n * `resolveApiKeyFromEnv` — which is the one place that reads the environment.\n *\n * Two reasons this is module state rather than fields on the parsed arguments, the same two\n * that put `auth_context` here: an extra key on `args` is rejected by every command calling\n * `.strict()`, and a hidden option to carry it would be a second undocumented way to pass a\n * credential. One process is one invocation, so there is nothing to get out of step.\n *\n * Capturing the environment here rather than reading it inside {@link selectCredential} keeps\n * the selection a function of its arguments. That is not tidiness: `ensureAuth` is called\n * directly by tests, and reading `process.env` down in the decision made those tests depend on\n * whether the developer running them happened to have `NEON_API_KEY` exported.\n */\nexport type CredentialInputs = {\n\tapiKeyFlag: string;\n\tapiKeyEnv: string;\n\tprofileEnv: string;\n};\n\nconst NO_INPUTS: CredentialInputs = {\n\tapiKeyFlag: \"\",\n\tapiKeyEnv: \"\",\n\tprofileEnv: \"\",\n};\n\nlet inputs: CredentialInputs = NO_INPUTS;\n\nexport const recordCredentialInputs = (recorded: CredentialInputs): void => {\n\tinputs = recorded;\n};\n\nexport const credentialInputs = (): CredentialInputs => inputs;\n\nexport const selectCredential = ({\n\tapiKeyFlag,\n\tprofileFlag,\n\tapiKeyEnv,\n\tprofileEnv,\n}: SelectionInput): CredentialSelection => {\n\tconst flagKey = nonEmpty(apiKeyFlag);\n\tconst flagProfile = nonEmpty(profileFlag);\n\n\tif (flagKey !== undefined && flagProfile !== undefined) {\n\t\tthrow new Error(\n\t\t\t\"Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.\",\n\t\t);\n\t}\n\n\tif (flagKey !== undefined) {\n\t\treturn { source: \"explicit-api-key\", apiKey: flagKey };\n\t}\n\n\tif (flagProfile !== undefined) {\n\t\treturn { source: \"profile\", profile: flagProfile, explicit: true };\n\t}\n\n\tconst envKey = nonEmpty(apiKeyEnv);\n\tconst envProfile = nonEmpty(profileEnv);\n\n\tif (envKey !== undefined) {\n\t\treturn {\n\t\t\tsource: \"ambient-api-key\",\n\t\t\tapiKey: envKey,\n\t\t\t...(envProfile !== undefined ? { ignoredProfile: envProfile } : {}),\n\t\t};\n\t}\n\n\treturn {\n\t\tsource: \"profile\",\n\t\tprofile: envProfile ?? DEFAULT_PROFILE,\n\t\texplicit: envProfile !== undefined,\n\t};\n};\n\n/** The warning for an ambient key that displaced an ambient profile, or `null`. */\nexport const displacedProfileWarning = (\n\tselection: CredentialSelection,\n): string | null =>\n\tselection.source === \"ambient-api-key\" &&\n\tselection.ignoredProfile !== undefined\n\t\t? `NEON_API_KEY is set, so profile \"${selection.ignoredProfile}\" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.`\n\t\t: null;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"auth_selection.js","names":[],"sources":["../../src/_shared/auth_selection.ts"],"sourcesContent":["/**\n * # Which credential an invocation authenticates with\n *\n * Four inputs can each answer \"who am I\": the `--api-key` flag, `NEON_API_KEY`, the\n * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so\n * the decision can be tested without a filesystem, a network, or a config directory.\n *\n * ## The rule\n *\n * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug\n * this module exists for: before it, any API key — including one merely exported into the\n * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as\n * whoever `NEON_API_KEY` belonged to and say nothing about it.\n *\n * | Given | What runs |\n * | --- | --- |\n * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |\n * | `--api-key` and `NEON_PROFILE` | the flag's key |\n * | `--profile` and `NEON_API_KEY` | the profile |\n * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |\n * | `--profile` or `NEON_PROFILE` alone | that profile |\n * | nothing | `DEFAULT` |\n *\n * Two explicit flags throw rather than picking a winner. They express different intents —\n * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no\n * reading of the command that makes both true, and guessing is how the original bug behaved.\n *\n * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline\n * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into\n * the environment. It warns instead of staying silent, because a disregarded account\n * selection is precisely what nobody noticed last time.\n *\n * `auth` and the `profile` subcommands do not use any of this. They read the same flags with\n * different meanings — `neon auth --profile work` names where to *write* a credential, and\n * `neon profile create work --api-key …` names one to *store* — so their callers skip\n * selection entirely rather than passing exemptions down here.\n */\n\nimport { DEFAULT_PROFILE } from \"./profiles.js\";\n\nexport type CredentialSelection =\n\t/** `--api-key`. Used as given; no profile is consulted and no stored file is touched. */\n\t| { source: \"explicit-api-key\"; apiKey: string }\n\t/** `NEON_API_KEY`, with the profile it displaced when there was one. */\n\t| { source: \"ambient-api-key\"; apiKey: string; ignoredProfile?: string }\n\t/** A profile, whose file decides whether that means an API key or OAuth. */\n\t| { source: \"profile\"; profile: string; explicit: boolean };\n\nexport type SelectionInput = {\n\t/** The `--api-key` flag, before any environment fallback has been folded into it. */\n\tapiKeyFlag?: string;\n\t/** The `--profile` flag. */\n\tprofileFlag?: string;\n\t/** `NEON_API_KEY`. */\n\tapiKeyEnv?: string;\n\t/** `NEON_PROFILE`. */\n\tprofileEnv?: string;\n};\n\n/**\n * What the four credential inputs were for this invocation, captured by\n * `resolveApiKeyFromEnv` — which is the one place that reads the environment.\n *\n * Two reasons this is module state rather than fields on the parsed arguments, the same two\n * that put `auth_context` here: an extra key on `args` is rejected by every command calling\n * `.strict()`, and a hidden option to carry it would be a second undocumented way to pass a\n * credential. One process is one invocation, so there is nothing to get out of step.\n *\n * Capturing the environment here rather than reading it inside {@link selectCredential} keeps\n * the selection a function of its arguments. That is not tidiness: `ensureAuth` is called\n * directly by tests, and reading `process.env` down in the decision made those tests depend on\n * whether the developer running them happened to have `NEON_API_KEY` exported.\n */\nexport type CredentialInputs = {\n\tapiKeyFlag: string;\n\tapiKeyEnv: string;\n\tprofileEnv: string;\n};\n\nconst NO_INPUTS: CredentialInputs = {\n\tapiKeyFlag: \"\",\n\tapiKeyEnv: \"\",\n\tprofileEnv: \"\",\n};\n\nlet inputs: CredentialInputs = NO_INPUTS;\n\nexport const recordCredentialInputs = (recorded: CredentialInputs): void => {\n\tinputs = recorded;\n};\n\nexport const credentialInputs = (): CredentialInputs => inputs;\n\nexport const selectCredential = ({\n\tapiKeyFlag,\n\tprofileFlag,\n\tapiKeyEnv,\n\tprofileEnv,\n}: SelectionInput): CredentialSelection => {\n\tconst flagKey = nonEmpty(apiKeyFlag);\n\tconst flagProfile = nonEmpty(profileFlag);\n\n\tif (flagKey !== undefined && flagProfile !== undefined) {\n\t\tthrow new Error(\n\t\t\t\"Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.\",\n\t\t);\n\t}\n\n\tif (flagKey !== undefined) {\n\t\treturn { source: \"explicit-api-key\", apiKey: flagKey };\n\t}\n\n\tif (flagProfile !== undefined) {\n\t\treturn { source: \"profile\", profile: flagProfile, explicit: true };\n\t}\n\n\tconst envKey = nonEmpty(apiKeyEnv);\n\tconst envProfile = nonEmpty(profileEnv);\n\n\tif (envKey !== undefined) {\n\t\treturn {\n\t\t\tsource: \"ambient-api-key\",\n\t\t\tapiKey: envKey,\n\t\t\t...(envProfile !== undefined ? { ignoredProfile: envProfile } : {}),\n\t\t};\n\t}\n\n\treturn {\n\t\tsource: \"profile\",\n\t\tprofile: envProfile ?? DEFAULT_PROFILE,\n\t\texplicit: envProfile !== undefined,\n\t};\n};\n\n/** The warning for an ambient key that displaced an ambient profile, or `null`. */\nexport const displacedProfileWarning = (\n\tselection: CredentialSelection,\n): string | null =>\n\tselection.source === \"ambient-api-key\" &&\n\tselection.ignoredProfile !== undefined\n\t\t? `NEON_API_KEY is set, so profile \"${selection.ignoredProfile}\" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.`\n\t\t: null;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,IAAI,SAA2B;CAL9B,YAAY;CACZ,WAAW;CACX,YAAY;AAG0B;AAEvC,MAAa,0BAA0B,aAAqC;CAC3E,SAAS;AACV;AAEA,MAAa,yBAA2C;AAExD,MAAa,oBAAoB,EAChC,YACA,aACA,WACA,iBAC0C;CAC1C,MAAM,UAAU,SAAS,UAAU;CACnC,MAAM,cAAc,SAAS,WAAW;CAExC,IAAI,YAAY,KAAA,KAAa,gBAAgB,KAAA,GAC5C,MAAM,IAAI,MACT,yHACD;CAGD,IAAI,YAAY,KAAA,GACf,OAAO;EAAE,QAAQ;EAAoB,QAAQ;CAAQ;CAGtD,IAAI,gBAAgB,KAAA,GACnB,OAAO;EAAE,QAAQ;EAAW,SAAS;EAAa,UAAU;CAAK;CAGlE,MAAM,SAAS,SAAS,SAAS;CACjC,MAAM,aAAa,SAAS,UAAU;CAEtC,IAAI,WAAW,KAAA,GACd,OAAO;EACN,QAAQ;EACR,QAAQ;EACR,GAAI,eAAe,KAAA,IAAY,EAAE,gBAAgB,WAAW,IAAI,CAAC;CAClE;CAGD,OAAO;EACN,QAAQ;EACR,SAAS,cAAA;EACT,UAAU,eAAe,KAAA;CAC1B;AACD;;AAGA,MAAa,2BACZ,cAEA,UAAU,WAAW,qBACrB,UAAU,mBAAmB,KAAA,IAC1B,oCAAoC,UAAU,eAAe,kDAAkD,UAAU,eAAe,uBACxI;AAEJ,SAAS,SAAS,OAA+C;CAChE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//#region src/_shared/credentials.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* # Stored credentials — one file per account, two kinds
|
|
4
|
+
*
|
|
5
|
+
* A profile points at exactly one credentials file (see `./profiles.ts`), and that file says
|
|
6
|
+
* what kind of credential it holds. Adding API-key support this way rather than adding a
|
|
7
|
+
* second pointer to `profiles.json` keeps a profile what it already was — one name, one path
|
|
8
|
+
* — and means `profiles.json` needs no schema change at all.
|
|
9
|
+
*
|
|
10
|
+
* ```json
|
|
11
|
+
* // oauth: every file written before this existed. An absent `type` means this.
|
|
12
|
+
* { "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
|
|
13
|
+
*
|
|
14
|
+
* // api_key, stored by `neon profile create --api-key`
|
|
15
|
+
* { "type": "api_key", "api_key": "napi_…", "user_id": "…" }
|
|
16
|
+
*
|
|
17
|
+
* // api_key minted by `--mint --org-id`, which records the scope it was issued at
|
|
18
|
+
* { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## One profile, one kind
|
|
22
|
+
*
|
|
23
|
+
* A credentials file holds an API key or an OAuth session, never both, and `type` states
|
|
24
|
+
* which. An earlier draft let the two coexist — the idea being that a key could keep the
|
|
25
|
+
* session it was minted from and so rotate without a browser. It did not survive review, for
|
|
26
|
+
* two reasons that are worth recording so nobody rebuilds it:
|
|
27
|
+
*
|
|
28
|
+
* 1. **It never worked.** The resolver returned the key without testing it, so a revoked key
|
|
29
|
+
* failed to mint and never fell back to the session sitting beside it.
|
|
30
|
+
* 2. **It could mix accounts.** Nothing compared the identity of the credential being written
|
|
31
|
+
* with the one already there, so a profile could hold one account's session and another's
|
|
32
|
+
* key, told apart only by a single string. Flip or lose `type` and the profile silently
|
|
33
|
+
* becomes a different person.
|
|
34
|
+
*
|
|
35
|
+
* Recovery from a dead key is therefore one browser login — `neon profile create <name>
|
|
36
|
+
* --mint --force` — which is what the retained session was supposed to save and never did.
|
|
37
|
+
*
|
|
38
|
+
* ## Older releases
|
|
39
|
+
*
|
|
40
|
+
* A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and
|
|
41
|
+
* looks for `access_token`. An `api_key` profile has none, so an older release falls through
|
|
42
|
+
* to its browser login rather than crashing. That it does not crash is why `credentials`
|
|
43
|
+
* stays a required pointer: an entry without one makes 2.41 and 2.42 throw
|
|
44
|
+
* `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.
|
|
45
|
+
*/
|
|
46
|
+
declare const OAUTH = "oauth";
|
|
47
|
+
declare const API_KEY = "api_key";
|
|
48
|
+
type CredentialKind = typeof OAUTH | typeof API_KEY;
|
|
49
|
+
/**
|
|
50
|
+
* The on-disk shape. Every field is optional because the two kinds overlap and because an
|
|
51
|
+
* OAuth token endpoint response carries more than we name here — the index signature keeps
|
|
52
|
+
* those extra fields on a round-trip rather than dropping them.
|
|
53
|
+
*/
|
|
54
|
+
type StoredCredentials = {
|
|
55
|
+
type?: string;
|
|
56
|
+
api_key?: string;
|
|
57
|
+
key_id?: number;
|
|
58
|
+
/** Set when the key was minted for an organization rather than the account. */
|
|
59
|
+
org_id?: string;
|
|
60
|
+
/** Set when the key was narrowed to a single project. Implies `org_id`. */
|
|
61
|
+
project_id?: string;
|
|
62
|
+
user_id?: string;
|
|
63
|
+
access_token?: string;
|
|
64
|
+
refresh_token?: string;
|
|
65
|
+
expires_at?: number;
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Where a credential lives, and which profile points at it.
|
|
70
|
+
*
|
|
71
|
+
* Both halves are needed to report a broken file: the path says which file to open, and the
|
|
72
|
+
* profile is what every recovery command takes as its argument. Carrying only the path is what
|
|
73
|
+
* produced errors telling the user to run `neon profile create <name> --force` with the
|
|
74
|
+
* placeholder intact — a command an agent will run verbatim and be told `Invalid profile name
|
|
75
|
+
* "<name>"`.
|
|
76
|
+
*/
|
|
77
|
+
type CredentialLocation = {
|
|
78
|
+
path: string;
|
|
79
|
+
profile: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Which credential in this file authenticates, by declaration alone.
|
|
83
|
+
*
|
|
84
|
+
* An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot
|
|
85
|
+
* interpret is a misconfiguration the user has to see: treating it as OAuth would send them
|
|
86
|
+
* to a browser login that silently replaces a credential they meant to keep, and treating it
|
|
87
|
+
* as an API key would authenticate with whatever `api_key` happened to be there.
|
|
88
|
+
*
|
|
89
|
+
* This deliberately does not check that an `api_key` file has a key — `neon profile list`
|
|
90
|
+
* needs the kind of a file it is not about to authenticate with, and must be able to report a
|
|
91
|
+
* broken one rather than throwing halfway through a table.
|
|
92
|
+
*/
|
|
93
|
+
declare const credentialKind: (credentials: StoredCredentials, at: CredentialLocation) => CredentialKind;
|
|
94
|
+
/** A credentials file resolved far enough to authenticate with. */
|
|
95
|
+
type InterpretedCredentials = {
|
|
96
|
+
kind: typeof API_KEY;
|
|
97
|
+
apiKey: string;
|
|
98
|
+
} | {
|
|
99
|
+
kind: typeof OAUTH;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Resolve what to authenticate with, validating that the declared kind is actually usable.
|
|
103
|
+
*
|
|
104
|
+
* An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user
|
|
105
|
+
* asked for a key, and quietly opening a browser instead would replace the credential they
|
|
106
|
+
* were trying to fix.
|
|
107
|
+
*/
|
|
108
|
+
declare const interpretCredentials: (credentials: StoredCredentials, at: CredentialLocation) => InterpretedCredentials;
|
|
109
|
+
/**
|
|
110
|
+
* Read a credentials file, or `null` when there is nothing usable there.
|
|
111
|
+
*
|
|
112
|
+
* Missing and unparseable both return `null`, because both are recoverable by authenticating
|
|
113
|
+
* again and the callers already treat "no credentials" as "log in". A file that parses but
|
|
114
|
+
* contradicts itself is different — {@link credentialKind} throws for that, since re-running
|
|
115
|
+
* `auth` would paper over a mistake rather than fix it.
|
|
116
|
+
*/
|
|
117
|
+
type CredentialsRead = {
|
|
118
|
+
kind: "ok";
|
|
119
|
+
credentials: StoredCredentials;
|
|
120
|
+
} | {
|
|
121
|
+
kind: "absent";
|
|
122
|
+
}
|
|
123
|
+
/** The file is there but cannot be understood. `reason` is safe to print. */ | {
|
|
124
|
+
kind: "unusable";
|
|
125
|
+
reason: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Read and classify a credentials file, without deciding what to do about it.
|
|
129
|
+
*
|
|
130
|
+
* A permission or I/O error still throws: there may be a perfectly good credential here that
|
|
131
|
+
* we cannot see, and treating that as absent would send the user to a browser login that
|
|
132
|
+
* overwrites it.
|
|
133
|
+
*/
|
|
134
|
+
declare const inspectCredentials: (path: string) => CredentialsRead;
|
|
135
|
+
/**
|
|
136
|
+
* The credential at `path`, or `null` when the file is not there.
|
|
137
|
+
*
|
|
138
|
+
* A damaged file is an error, not an absence. Treating it as absent — which is what this used to
|
|
139
|
+
* do — meant any read-only command could repair it by starting a browser sign-in and overwriting
|
|
140
|
+
* it, **possibly as a different account**, with the user never having asked for a repair and no
|
|
141
|
+
* way back to whatever was in the file. Failing here costs one deliberate command; the message
|
|
142
|
+
* names it.
|
|
143
|
+
*
|
|
144
|
+
* `profile list` and telemetry use {@link inspectCredentials} instead, because describing a
|
|
145
|
+
* broken credential is not the same as using one.
|
|
146
|
+
*/
|
|
147
|
+
declare const readCredentials: (at: CredentialLocation) => StoredCredentials | null;
|
|
148
|
+
declare const writeCredentials: (path: string, credentials: StoredCredentials) => void;
|
|
149
|
+
/** The scope a minted key was issued at. Absent org means an account key. */
|
|
150
|
+
type KeyScope = {
|
|
151
|
+
orgId?: string;
|
|
152
|
+
projectId?: string;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Build an `api_key` credentials object. Nothing from a previous credential is carried over.
|
|
156
|
+
*
|
|
157
|
+
* The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint
|
|
158
|
+
* the replacement on the same endpoint, and an org or project key minted as an account key
|
|
159
|
+
* would silently widen what the profile reaches.
|
|
160
|
+
*/
|
|
161
|
+
declare const apiKeyCredentials: ({
|
|
162
|
+
apiKey,
|
|
163
|
+
keyId,
|
|
164
|
+
userId,
|
|
165
|
+
scope
|
|
166
|
+
}: {
|
|
167
|
+
apiKey: string;
|
|
168
|
+
keyId?: number;
|
|
169
|
+
userId?: string;
|
|
170
|
+
scope?: KeyScope;
|
|
171
|
+
}) => StoredCredentials;
|
|
172
|
+
/** The scope recorded on a stored credential. */
|
|
173
|
+
declare const scopeOf: (credentials: StoredCredentials) => KeyScope;
|
|
174
|
+
/** How to describe a scope in output. */
|
|
175
|
+
declare const describeScope: (scope: KeyScope) => string;
|
|
176
|
+
/**
|
|
177
|
+
* Whether a stored credential is the same secret as the one about to replace it.
|
|
178
|
+
*
|
|
179
|
+
* Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it
|
|
180
|
+
* would revoke the credential the command has just committed to. Trimmed on both sides, because
|
|
181
|
+
* a key read from a file or a pipe arrives with a trailing newline.
|
|
182
|
+
*/
|
|
183
|
+
declare const isSameCredential: (existingKey: string | undefined, replacementKey: string | undefined) => boolean;
|
|
184
|
+
//#endregion
|
|
185
|
+
export { API_KEY, CredentialKind, CredentialLocation, CredentialsRead, InterpretedCredentials, KeyScope, OAUTH, StoredCredentials, apiKeyCredentials, credentialKind, describeScope, inspectCredentials, interpretCredentials, isSameCredential, readCredentials, scopeOf, writeCredentials };
|
|
186
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","names":[],"sources":["../../src/_shared/credentials.ts"],"mappings":";;AAgDA;AACA;AAEA;AAA0B;AAAU;AAAe;AAAO;AAO1D;AAwBA;AAiBA;AAaC;AAZa;AACT;AACF;AAUF;AAYD;AAAkC;AAChB;AACA;AAAK;AASvB;AAYC;AAXa;AACT;AACF;AASF;AAUD;AAaA;AAiDA;AAQC;AAPI;AACF;AAAiB;AAQpB;AAQA;AAYA;AAiBE;AAjBgC;AAAA;AAAA;AAAA;AASzB;AACL;AAOF,cA1NW,KAAA,GA0NX,OAAA;AAGW,cA5NA,OAAA,GAmOX,SAAA;AAAA,KAjOU,cAAA,GAiOV,OAjOkC,KAiOlC,GAAA,OAjOiD,OAiOjD;AAPmC;AAAoB;AAOvD;AAGF;AAmBA;KAhPY,iBAAA;;;;;;;;;;;;;;;;;;;;;;;KAwBA,kBAAA;;;;;;;;;;;;;;;;cAiBC,8BACC,uBACT,uBACF;;KAsBS,sBAAA;eACM;;;eACA;;;;;;;;;cASL,oCACC,uBACT,uBACF;;;;;;;;;KAmBS,eAAA;;eACkB;;;;;;;;;;;;;;;cAYjB,sCAAqC;;;;;;;;;;;;;cAiDrC,sBACR,uBACF;cAQU,8CAEC;;KAMF,QAAA;;;;;;;;;;;cAYC;;;;;;;;;UASJ;MACL;;cAUS,uBAAwB,sBAAoB;;cAU5C,uBAAwB;;;;;;;;cAmBxB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { writeSecretFile } from "./secure_file.js";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
//#region src/_shared/credentials.ts
|
|
3
4
|
/**
|
|
@@ -120,12 +121,70 @@ const inspectCredentials = (path) => {
|
|
|
120
121
|
credentials: parsed
|
|
121
122
|
};
|
|
122
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* The credential at `path`, or `null` when the file is not there.
|
|
126
|
+
*
|
|
127
|
+
* A damaged file is an error, not an absence. Treating it as absent — which is what this used to
|
|
128
|
+
* do — meant any read-only command could repair it by starting a browser sign-in and overwriting
|
|
129
|
+
* it, **possibly as a different account**, with the user never having asked for a repair and no
|
|
130
|
+
* way back to whatever was in the file. Failing here costs one deliberate command; the message
|
|
131
|
+
* names it.
|
|
132
|
+
*
|
|
133
|
+
* `profile list` and telemetry use {@link inspectCredentials} instead, because describing a
|
|
134
|
+
* broken credential is not the same as using one.
|
|
135
|
+
*/
|
|
136
|
+
const readCredentials = (at) => {
|
|
137
|
+
const read = inspectCredentials(at.path);
|
|
138
|
+
if (read.kind === "unusable") throw new Error(`${read.reason}. ${repair(at)}`);
|
|
139
|
+
return read.kind === "ok" ? read.credentials : null;
|
|
140
|
+
};
|
|
141
|
+
const writeCredentials = (path, credentials) => {
|
|
142
|
+
writeSecretFile(path, JSON.stringify(credentials));
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Build an `api_key` credentials object. Nothing from a previous credential is carried over.
|
|
146
|
+
*
|
|
147
|
+
* The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint
|
|
148
|
+
* the replacement on the same endpoint, and an org or project key minted as an account key
|
|
149
|
+
* would silently widen what the profile reaches.
|
|
150
|
+
*/
|
|
151
|
+
const apiKeyCredentials = ({ apiKey, keyId, userId, scope }) => ({
|
|
152
|
+
type: API_KEY,
|
|
153
|
+
api_key: apiKey,
|
|
154
|
+
...keyId !== void 0 ? { key_id: keyId } : {},
|
|
155
|
+
...userId !== void 0 ? { user_id: userId } : {},
|
|
156
|
+
...scope?.orgId !== void 0 ? { org_id: scope.orgId } : {},
|
|
157
|
+
...scope?.projectId !== void 0 ? { project_id: scope.projectId } : {}
|
|
158
|
+
});
|
|
159
|
+
/** The scope recorded on a stored credential. */
|
|
160
|
+
const scopeOf = (credentials) => ({
|
|
161
|
+
...typeof credentials.org_id === "string" ? { orgId: credentials.org_id } : {},
|
|
162
|
+
...typeof credentials.project_id === "string" ? { projectId: credentials.project_id } : {}
|
|
163
|
+
});
|
|
164
|
+
/** How to describe a scope in output. */
|
|
165
|
+
const describeScope = (scope) => {
|
|
166
|
+
if (scope.projectId !== void 0) return `project ${scope.projectId}`;
|
|
167
|
+
if (scope.orgId !== void 0) return `org ${scope.orgId}`;
|
|
168
|
+
return "account";
|
|
169
|
+
};
|
|
123
170
|
function nonEmpty(value) {
|
|
124
171
|
if (typeof value !== "string") return void 0;
|
|
125
172
|
const trimmed = value.trim();
|
|
126
173
|
return trimmed === "" ? void 0 : trimmed;
|
|
127
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether a stored credential is the same secret as the one about to replace it.
|
|
177
|
+
*
|
|
178
|
+
* Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it
|
|
179
|
+
* would revoke the credential the command has just committed to. Trimmed on both sides, because
|
|
180
|
+
* a key read from a file or a pipe arrives with a trailing newline.
|
|
181
|
+
*/
|
|
182
|
+
const isSameCredential = (existingKey, replacementKey) => {
|
|
183
|
+
if (existingKey === void 0 || replacementKey === void 0) return false;
|
|
184
|
+
const trimmed = existingKey.trim();
|
|
185
|
+
return trimmed !== "" && trimmed === replacementKey.trim();
|
|
186
|
+
};
|
|
128
187
|
//#endregion
|
|
129
|
-
export { API_KEY, OAUTH, credentialKind, inspectCredentials, interpretCredentials };
|
|
188
|
+
export { API_KEY, OAUTH, apiKeyCredentials, credentialKind, describeScope, inspectCredentials, interpretCredentials, isSameCredential, readCredentials, scopeOf, writeCredentials };
|
|
130
189
|
|
|
131
190
|
//# sourceMappingURL=credentials.js.map
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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;;;;;;;;;;;;;AAcA,MAAa,mBACZ,OAC8B;CAC9B,MAAM,OAAO,mBAAmB,GAAG,IAAI;CACvC,IAAI,KAAK,SAAS,YACjB,MAAM,IAAI,MAAM,GAAG,KAAK,OAAO,IAAI,OAAO,EAAE,GAAG;CAEhD,OAAO,KAAK,SAAS,OAAO,KAAK,cAAc;AAChD;AAEA,MAAa,oBACZ,MACA,gBACU;CACV,gBAAgB,MAAM,KAAK,UAAU,WAAW,CAAC;AAClD;;;;;;;;AAeA,MAAa,qBAAqB,EACjC,QACA,OACA,QACA,aAMyB;CACzB,MAAM;CACN,SAAS;CACT,GAAI,UAAU,KAAA,IAAY,EAAE,QAAQ,MAAM,IAAI,CAAC;CAC/C,GAAI,WAAW,KAAA,IAAY,EAAE,SAAS,OAAO,IAAI,CAAC;CAClD,GAAI,OAAO,UAAU,KAAA,IAAY,EAAE,QAAQ,MAAM,MAAM,IAAI,CAAC;CAC5D,GAAI,OAAO,cAAc,KAAA,IAAY,EAAE,YAAY,MAAM,UAAU,IAAI,CAAC;AACzE;;AAGA,MAAa,WAAW,iBAA8C;CACrE,GAAI,OAAO,YAAY,WAAW,WAC/B,EAAE,OAAO,YAAY,OAAO,IAC5B,CAAC;CACJ,GAAI,OAAO,YAAY,eAAe,WACnC,EAAE,WAAW,YAAY,WAAW,IACpC,CAAC;AACL;;AAGA,MAAa,iBAAiB,UAA4B;CACzD,IAAI,MAAM,cAAc,KAAA,GAAW,OAAO,WAAW,MAAM;CAC3D,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,OAAO,MAAM;CACnD,OAAO;AACR;AAEA,SAAS,SAAS,OAAoC;CACrD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,YAAY,KAAK,KAAA,IAAY;AACrC;;;;;;;;AASA,MAAa,oBACZ,aACA,mBACa;CACb,IAAI,gBAAgB,KAAA,KAAa,mBAAmB,KAAA,GAAW,OAAO;CACtE,MAAM,UAAU,YAAY,KAAK;CACjC,OAAO,YAAY,MAAM,YAAY,eAAe,KAAK;AAC1D"}
|