@neondatabase/env 0.15.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +8 -29
  2. package/dist/_shared/auth_selection.d.ts +95 -0
  3. package/dist/_shared/auth_selection.d.ts.map +1 -0
  4. package/dist/_shared/auth_selection.js +10 -1
  5. package/dist/_shared/auth_selection.js.map +1 -1
  6. package/dist/_shared/credentials.d.ts +186 -0
  7. package/dist/_shared/credentials.d.ts.map +1 -0
  8. package/dist/_shared/credentials.js +60 -1
  9. package/dist/_shared/credentials.js.map +1 -1
  10. package/dist/{lib → _shared/env-core}/env.d.ts +7 -92
  11. package/dist/_shared/env-core/env.d.ts.map +1 -0
  12. package/dist/{lib → _shared/env-core}/env.js +14 -184
  13. package/dist/_shared/env-core/env.js.map +1 -0
  14. package/dist/{lib → _shared/env-core}/reuse-secrets.d.ts +4 -4
  15. package/dist/_shared/env-core/reuse-secrets.d.ts.map +1 -0
  16. package/dist/{lib → _shared/env-core}/reuse-secrets.js +2 -2
  17. package/dist/_shared/env-core/reuse-secrets.js.map +1 -0
  18. package/dist/_shared/paths.d.ts +116 -0
  19. package/dist/_shared/paths.d.ts.map +1 -0
  20. package/dist/_shared/paths.js +23 -1
  21. package/dist/_shared/paths.js.map +1 -1
  22. package/dist/_shared/profiles.d.ts +140 -2
  23. package/dist/_shared/profiles.d.ts.map +1 -1
  24. package/dist/_shared/profiles.js +106 -2
  25. package/dist/_shared/profiles.js.map +1 -1
  26. package/dist/_shared/secure_file.d.ts +25 -0
  27. package/dist/_shared/secure_file.d.ts.map +1 -0
  28. package/dist/_shared/secure_file.js +43 -0
  29. package/dist/_shared/secure_file.js.map +1 -0
  30. package/dist/config/dist/lib/types.d.ts +92 -33
  31. package/dist/config/dist/lib/types.d.ts.map +1 -1
  32. package/dist/config/dist/v1.d.ts +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +2 -1
  35. package/dist/lib/cli/commands.js +1 -1
  36. package/dist/lib/cli/commands.js.map +1 -1
  37. package/dist/lib/parse-env.d.ts +95 -0
  38. package/dist/lib/parse-env.d.ts.map +1 -0
  39. package/dist/lib/parse-env.js +198 -0
  40. package/dist/lib/parse-env.js.map +1 -0
  41. package/package.json +4 -9
  42. package/dist/lib/env.d.ts.map +0 -1
  43. package/dist/lib/env.js.map +0 -1
  44. package/dist/lib/reuse-secrets.d.ts.map +0 -1
  45. package/dist/lib/reuse-secrets.js.map +0 -1
  46. package/dist/runtime.d.ts +0 -2
  47. 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
- ## Two entry points
15
+ ## What's in it
16
16
 
17
- | Import | What it holds |
18
- | --- | --- |
19
- | `@neon/env` | The pure half: `fetchEnv` asks the Neon API for a branch's env, `parseEnv` reads what was already injected. Neither touches the filesystem or reads an env source. This is what an app, a build script, or a `neon.ts` policy needs. |
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
- The split mirrors [`@neon/config`](../config) vs [`@neon/config-runtime`](../config-runtime), for the same reason: side effects should be something you opt into by import path. Contributing to either half? See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
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 }`. Takes an optional `revokeSuperseded`, which keeps the replaced credential live when your call resolves only part of the branch. 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,30 +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
- `fetchEnvReusingSecrets`, from the **`@neon/env/runtime`** entry point, is the wrapper that avoids that. It checks what you already hold, keeps what is still valid, and asks `fetchEnv` for only the rest:
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 — ids it replaced and revoked
153
- // credential.superseded — ids it replaced but left live (`revokeSuperseded: false`)
154
- }
155
- ```
156
-
157
- The check is a real verification, not a presence test. A persisted secret is kept only when it names a credential that still exists on the branch, isn't revoked or expired, and carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in the console, one copied from another branch, or one predating a newly-enabled feature all fail that check and get replaced — and the credential being replaced is revoked, so a branch doesn't accumulate one per call.
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.
158
137
 
159
- No local bookkeeping backs this: `AWS_ACCESS_KEY_ID` **is** the credential's token id, and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`, so the persisted secrets already name the credential that issued them.
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.
160
139
 
161
- Revoking is only safe because the call resolves everything the policy enables. Pass `revokeSuperseded: false` when yours resolves a **subset** — the credential your persisted secrets name may also back a service you are not resolving, and revoking it would break that service while its vars, which you are not rewriting, stay in place. The cost is an orphaned credential, which is the safer of the two failures, and `credential.superseded` names it so you can report it rather than leave it invisible. `neon env pull --service` is the caller this exists for.
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.
162
141
 
163
142
  ### Fetching a subset
164
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,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"}
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"}