neon 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,223 +0,0 @@
1
- import { credentialScopesSatisfied, } from "@neon/config/v1";
2
- import { createApiFromOptions, credentialEnvKeys, credentialName, fetchEnvKeys, NEON_ENV_VAR_KEYS, policyEnvKeys, previewCredentialScopes, resolveBranchPolicy, toEntries, } from "./env.js";
3
- /**
4
- * Resolve a branch's env while keeping one-time secrets the caller already holds.
5
- *
6
- * {@link fetchEnvKeys} — and the public `fetchEnv` — only ever *fetch*. The Neon API returns a
7
- * credential's `api_token` / `s3_secret_access_key` exactly once, at mint time, so "fetching"
8
- * them means minting a new credential; a plain `fetchEnv` on every `neon dev` start or `env
9
- * pull` would leave a live credential behind each time. This is the wrapper that avoids that:
10
- * it looks at what the caller already has, decides what is still usable, and asks `fetchEnv`
11
- * for only the rest.
12
- *
13
- * The check is a real verification, not a presence test. A persisted secret is kept only when
14
- * it names a credential that still exists on this branch, is not revoked or expired, and
15
- * carries every scope the policy needs. A `.env.example` placeholder, a credential revoked in
16
- * the console, one copied in from another branch, or one predating a newly-enabled feature all
17
- * fail that check and get replaced.
18
- *
19
- * None of this needs local bookkeeping, because the secrets carry their own credential id:
20
- * `AWS_ACCESS_KEY_ID` **is** the credential's `tokenId` (the storage gateway authenticates
21
- * against the full id), and the AI Gateway token is minted as `nt_live_<tokenIdShort>_<secret>`,
22
- * where `tokenIdShort` is what the credentials list reports. The env source being replaced is
23
- * the record of what the last call issued.
24
- *
25
- * ```ts
26
- * import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
27
- *
28
- * const { vars, credential } = await fetchEnvReusingSecrets(config, {
29
- * projectId,
30
- * branch: "main",
31
- * env: { ...process.env, ...readEnvFile(".env") },
32
- * });
33
- * if (credential.issued) console.log(`new values for ${credential.keys.join(", ")}`);
34
- * ```
35
- */
36
- export async function fetchEnvReusingSecrets(config, options) {
37
- const { env: source = process.env, revokeSuperseded = true, ...fetchOptions } = options;
38
- const api = options.api ?? createApiFromOptions(options);
39
- const { branch, desired } = await resolveBranchPolicy(config, options, api);
40
- const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
41
- const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
42
- const secretKeys = credentialEnvKeys({
43
- storage: storageEnabled,
44
- aiGateway: gatewayEnabled,
45
- });
46
- // Nothing credential-backed on this branch, so there is nothing to preserve and no
47
- // credential to spend: fetch everything and skip the credentials endpoint entirely.
48
- if (secretKeys.length === 0) {
49
- const fetched = await fetchEnvKeys(config, fetchOptions, null);
50
- return {
51
- vars: preferPersisted(toEntries(fetched), source),
52
- credential: {
53
- issued: false,
54
- keys: [],
55
- revoked: [],
56
- superseded: [],
57
- },
58
- };
59
- }
60
- const persisted = readPersistedSecrets(source);
61
- const complete = (!storageEnabled ||
62
- Boolean(persisted.accessKeyId && persisted.secretAccessKey)) &&
63
- (!gatewayEnabled || Boolean(persisted.apiToken));
64
- // Look the persisted secrets up whenever there are any — not only when they're complete.
65
- // An incomplete set still names the credential a newly-enabled feature is about to
66
- // supersede (a storage-only credential on a branch that just gained the AI Gateway), and
67
- // that one should be revoked rather than left live.
68
- const named = persisted.accessKeyId !== "" || persisted.apiToken !== ""
69
- ? namedCredentials(await api.listCredentials(options.projectId, branch.id), persisted)
70
- : { storage: null, gateway: null };
71
- const reusable = complete
72
- ? reusableCredential(named, { storageEnabled, gatewayEnabled })
73
- : null;
74
- const scopes = previewCredentialScopes(desired.preview);
75
- const keep = reusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);
76
- // Ask for everything the policy produces, minus the secrets we're keeping — which is what
77
- // stops `fetchEnv` from minting a credential it doesn't need.
78
- const allKeys = policyEnvKeys(desired);
79
- const fetchKeys = keep
80
- ? allKeys.filter((key) => !secretKeys.includes(key))
81
- : allKeys;
82
- const fetched = await fetchEnvKeys(config,
83
- // Pass the resolved id so `fetchEnv` targets the same branch this call verified against,
84
- // even if `options.branch` was a name that has since been reused.
85
- { ...fetchOptions, branchId: branch.id, api }, fetchKeys);
86
- const vars = preferPersisted(toEntries(fetched), source);
87
- if (keep) {
88
- for (const key of secretKeys) {
89
- const value = source[key];
90
- if (value !== undefined)
91
- vars[key] = value;
92
- }
93
- return {
94
- vars,
95
- credential: {
96
- issued: false,
97
- keys: secretKeys,
98
- revoked: [],
99
- superseded: [],
100
- },
101
- };
102
- }
103
- // A replacement was minted, so revoke what it supersedes: the credentials the old secrets
104
- // named, minus any this tool did not issue. Their secrets lived nowhere but the env source
105
- // this call replaces, so revoking them strands nothing — and it keeps a branch from
106
- // accumulating a live credential per call. Everything else on the branch is left alone: it
107
- // may belong to a teammate, another checkout, or a deployed function, and nothing
108
- // observable distinguishes those from an orphan of our own.
109
- //
110
- // Revoked *after* the fetch, so a failed fetch leaves the caller's existing secrets working.
111
- const ours = new Set();
112
- for (const meta of [named.storage, named.gateway]) {
113
- if (meta !== null &&
114
- meta.principalType === "user" &&
115
- meta.name === credentialName(branch.name)) {
116
- ours.add(meta.tokenId);
117
- }
118
- }
119
- if (revokeSuperseded) {
120
- for (const tokenId of ours) {
121
- await api.revokeCredential(options.projectId, branch.id, tokenId);
122
- }
123
- }
124
- return {
125
- vars,
126
- credential: {
127
- issued: true,
128
- keys: secretKeys,
129
- revoked: revokeSuperseded ? [...ours] : [],
130
- superseded: revokeSuperseded ? [] : [...ours],
131
- },
132
- };
133
- }
134
- /** Read the branch credential's secrets out of an env source. */
135
- function readPersistedSecrets(source) {
136
- const storage = NEON_ENV_VAR_KEYS.storage;
137
- const gateway = NEON_ENV_VAR_KEYS.aiGateway;
138
- return {
139
- accessKeyId: source[storage.accessKeyId] ?? "",
140
- secretAccessKey: source[storage.secretAccessKey] ?? "",
141
- apiToken: source[gateway.apiKey] ?? "",
142
- };
143
- }
144
- /**
145
- * Keep a persisted value rather than overwriting it with an empty fetched one.
146
- *
147
- * Neon Auth's `base_url` is the case that needs this: integrations created before the API
148
- * returned it answer with an empty string, and the persisted copy is the only one left. An
149
- * empty fetched value never carries more information than a non-empty persisted one, so
150
- * preferring the latter is safe for every var — and it keeps a pull from blanking a working
151
- * line in someone's `.env`.
152
- */
153
- function preferPersisted(vars, source) {
154
- const out = { ...vars };
155
- for (const [key, value] of Object.entries(out)) {
156
- if (value !== "")
157
- continue;
158
- const persisted = source[key];
159
- if (persisted !== undefined && persisted !== "")
160
- out[key] = persisted;
161
- }
162
- return out;
163
- }
164
- /**
165
- * The credential id embedded in an AI Gateway token. The API mints them as
166
- * `nt_live_<tokenIdShort>_<secret>`, and `tokenIdShort` is the public identifier the credentials
167
- * list reports — so a persisted token names the credential that issued it. Returns `null` for
168
- * anything not in that shape (a `.env.example` placeholder, a hand-typed value), which callers
169
- * treat as unverifiable.
170
- */
171
- function gatewayTokenIdShort(apiToken) {
172
- return /^nt_live_([^_]+)_.+$/.exec(apiToken)?.[1] ?? null;
173
- }
174
- /** Whether an issued credential can still be used: not revoked, not past its expiry. */
175
- function isLiveCredential(meta, now) {
176
- if (meta.revokedAt !== undefined)
177
- return false;
178
- if (meta.expiresAt === undefined)
179
- return true;
180
- const expiresAt = Date.parse(meta.expiresAt);
181
- return Number.isNaN(expiresAt) || expiresAt > now;
182
- }
183
- /**
184
- * The live credentials the persisted secrets name — at most one per half. A half that names
185
- * nothing contributes nothing, which is what a placeholder, a credential revoked in the
186
- * console, and one copied in from another branch all look like from here.
187
- */
188
- function namedCredentials(live, persisted) {
189
- const usable = live.filter((meta) => isLiveCredential(meta, Date.now()));
190
- const shortId = persisted.apiToken
191
- ? gatewayTokenIdShort(persisted.apiToken)
192
- : null;
193
- return {
194
- storage: persisted.accessKeyId
195
- ? (usable.find((meta) => meta.tokenId === persisted.accessKeyId) ??
196
- null)
197
- : null,
198
- gateway: shortId
199
- ? (usable.find((meta) => meta.tokenIdShort === shortId) ?? null)
200
- : null,
201
- };
202
- }
203
- /**
204
- * The credential the persisted secrets can be *reused* as, or `null`.
205
- *
206
- * Strict on purpose: every half the policy enables has to name a live credential, and when both
207
- * features are enabled they must name the *same* one — they share a single credential, so
208
- * halves that disagree came from two different calls and neither can be trusted.
209
- */
210
- function reusableCredential(named, enabled) {
211
- if (enabled.storageEnabled && enabled.gatewayEnabled) {
212
- return named.storage &&
213
- named.gateway &&
214
- named.storage.tokenId === named.gateway.tokenId
215
- ? named.storage
216
- : null;
217
- }
218
- if (enabled.storageEnabled)
219
- return named.storage;
220
- if (enabled.gatewayEnabled)
221
- return named.gateway;
222
- return null;
223
- }
@@ -1,148 +0,0 @@
1
- /**
2
- * # Where the Neon CLIs keep their files on disk
3
- *
4
- **Deliberately impure.** It reads environment variables and touches the filesystem, which
5
- * `@neon/config` — the package this used to be a subpath of — must never do from its root
6
- * export. It lives here instead of there precisely so that a policy-facing package does not
7
- * carry implementor-only code.
8
- *
9
- * It exists because three separate readers each grew their own answer to "where is the
10
- * config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
11
- * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
12
- * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
13
- * credentials somewhere the other two never looked.
14
- *
15
- * ## The directory
16
- *
17
- * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
18
- * each entry winning over the next:
19
- *
20
- * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
21
- * 2. `NEON_CONFIG_DIR` — exact.
22
- * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
23
- * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
24
- *
25
- * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
26
- * quietly read `~/.config/neonctl` would defeat the point of passing it.
27
- *
28
- * ## The files
29
- *
30
- * {@link resolveConfigFile} answers "which path should I use for this file", and it is the
31
- * same answer for reading and writing:
32
- *
33
- * - Present in `neon/` → use it.
34
- * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
35
- * never copied or moved, so nothing is left behind to go stale and no other tool starts
36
- * reading an abandoned token.
37
- * - Present in neither → the new location. New files only ever appear under `neon/`.
38
- */
39
- import { existsSync } from "node:fs";
40
- import { join, resolve } from "node:path";
41
- /** Current directory name. New files are created here. */
42
- export const CONFIG_DIR_NAME = "neon";
43
- /** Legacy directory name, read forever so existing installs keep working untouched. */
44
- export const LEGACY_CONFIG_DIR_NAME = "neonctl";
45
- /** Where files are created. See the module docs for the precedence. */
46
- export function configDir(options = {}) {
47
- const explicit = explicitDir(options);
48
- if (explicit)
49
- return explicit;
50
- return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
51
- }
52
- /**
53
- * The legacy directory, or `undefined` when the location was chosen explicitly (in which
54
- * case there is no legacy counterpart to fall back to).
55
- */
56
- export function legacyConfigDir(options = {}) {
57
- if (explicitDir(options))
58
- return undefined;
59
- return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
60
- }
61
- /**
62
- * Resolve one file inside the config directory. Prefers the current location, falls back to
63
- * an existing legacy file **in place**, and otherwise points at the current location so new
64
- * files are created there.
65
- */
66
- export function resolveConfigFile(fileName, options = {}) {
67
- const dir = configDir(options);
68
- const current = resolve(dir, fileName);
69
- if (existsSync(current))
70
- return { path: current, dir, isLegacy: false, exists: true };
71
- const legacyDir = legacyConfigDir(options);
72
- if (legacyDir) {
73
- const legacy = resolve(legacyDir, fileName);
74
- if (existsSync(legacy))
75
- return {
76
- path: legacy,
77
- dir: legacyDir,
78
- isLegacy: true,
79
- exists: true,
80
- };
81
- }
82
- return { path: current, dir, isLegacy: false, exists: false };
83
- }
84
- /** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
85
- function configHome(env) {
86
- const xdg = nonEmpty(env.XDG_CONFIG_HOME);
87
- if (xdg)
88
- return xdg;
89
- const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
90
- return home ? join(home, ".config") : ".config";
91
- }
92
- function explicitDir(options) {
93
- const env = options.env ?? process.env;
94
- return (nonEmpty(options.dir) ??
95
- nonEmpty(env.NEON_CONFIG_DIR) ??
96
- nonEmpty(env.NEONCTL_CONFIG_DIR));
97
- }
98
- function nonEmpty(value) {
99
- if (typeof value !== "string")
100
- return undefined;
101
- const trimmed = value.trim();
102
- return trimmed === "" ? undefined : trimmed;
103
- }
104
- export const CREDENTIALS_FILE = "credentials.json";
105
- /**
106
- * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
107
- *
108
- * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
109
- * see {@link credentialsPath} — but it is never written to, moved, or deleted.
110
- */
111
- export const defaultDir = configDir();
112
- /**
113
- * Where this invocation's `credentials.json` lives.
114
- *
115
- * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
116
- * directory is used **in place**: an install that predates the rename keeps working, and its
117
- * credentials are never duplicated into a second location where one copy could go stale while
118
- * another tool still reads it.
119
- *
120
- * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
121
- * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
122
- * scratch directory must never pick up a developer's real credentials.
123
- */
124
- export const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
125
- /**
126
- * Whether a credentials file is one the CLI created, rather than a path a profile adopted.
127
- *
128
- * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
129
- * that is what makes adopting an existing directory a one-line edit — and a file we did not
130
- * create is not ours to remove.
131
- */
132
- export const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
133
- /**
134
- * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
135
- *
136
- * {@link credentialsPath} deliberately reads an existing legacy file in place rather than
137
- * migrating it, so for a default config directory that file is ours even though it sits outside
138
- * `neon/`. Judging ownership on the current directory alone would call an install that predates
139
- * the rename "adopted".
140
- */
141
- export const isOwnedCredentialPath = (configDirectory, file) => {
142
- if (isInsideConfigDir(configDirectory, file))
143
- return true;
144
- if (configDirectory !== defaultDir)
145
- return false;
146
- const legacy = legacyConfigDir();
147
- return legacy !== undefined && isInsideConfigDir(legacy, file);
148
- };
@@ -1,276 +0,0 @@
1
- /**
2
- * # Profiles — several Neon accounts in one config directory
3
- *
4
- * A profile is **a pointer to a credentials file**. Nothing more. That constraint is what
5
- * keeps the feature small: there is no mirror, no per-profile directory tree, no persistent
6
- * "active profile" state to fall out of sync, and no migration.
7
- *
8
- * ```
9
- * ~/.config/neon/
10
- * ├── credentials.json # this IS the DEFAULT profile, not a copy of it
11
- * ├── credentials.work.json # created by `neon auth --profile work`
12
- * └── profiles.json # created only once a second profile exists
13
- * ```
14
- *
15
- * `profiles.json` maps a name to a path, and the path may point anywhere — which is what
16
- * makes adopting an existing directory a one-line edit rather than an import command:
17
- *
18
- * ```json
19
- * {
20
- * "version": 1,
21
- * "profiles": {
22
- * "DEFAULT": { "credentials": "credentials.json" },
23
- * "work": {
24
- * "credentials": "../neonctl-databricks/credentials.json",
25
- * "label": "someone@example.com"
26
- * }
27
- * }
28
- * }
29
- * ```
30
- *
31
- * ## Selection
32
- *
33
- * `--profile` → `NEON_PROFILE` → `DEFAULT`. Per invocation, like `AWS_PROFILE`; there is no
34
- * `profile use` command, so nothing persists that could disagree with what you typed.
35
- *
36
- * ## Compatibility
37
- *
38
- * An install with no `profiles.json` is already a valid `DEFAULT`-only state: `DEFAULT`
39
- * resolves to `credentials.json` in the config directory (including an existing one in the
40
- * legacy `neonctl` directory — see `./paths.ts`). Nothing is created until a second
41
- * profile is, and nothing is ever moved.
42
- */
43
- import { existsSync, readFileSync } from "node:fs";
44
- import { isAbsolute, relative, resolve } from "node:path";
45
- import { credentialsPath, defaultDir, resolveConfigFile } from "./paths.js";
46
- import { writeSecretFile } from "./secure_file.js";
47
- export const PROFILES_FILE = "profiles.json";
48
- /** The implicit profile. Backed by plain `credentials.json`, with or without a profiles file. */
49
- export const DEFAULT_PROFILE = "DEFAULT";
50
- /** Profile names become part of a filename, so keep them boring. */
51
- const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
52
- /** Which profile this invocation should use: `--profile` → `NEON_PROFILE` → `DEFAULT`. */
53
- export const selectProfileName = (flag, env = process.env) => nonEmpty(flag) ?? nonEmpty(env.NEON_PROFILE) ?? DEFAULT_PROFILE;
54
- export const assertValidProfileName = (name) => {
55
- if (!NAME_PATTERN.test(name)) {
56
- throw new Error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore, starting with a letter or digit.`);
57
- }
58
- };
59
- /** Where `profiles.json` lives for this config directory (whether or not it exists yet). */
60
- export const profilesFilePath = (dir) => resolveConfigFile(PROFILES_FILE, dir === defaultDir ? {} : { dir }).path;
61
- /**
62
- * Read and classify `profiles.json` without deciding what to do about it.
63
- *
64
- * Entry keys and shapes are validated here rather than at each use. A key is a profile name,
65
- * and a name that `assertValidProfileName` would reject cannot have been written by this CLI —
66
- * it would travel into error messages as a recovery command nobody can run, and into a
67
- * `credentials.<name>.json` filename.
68
- */
69
- export const inspectProfiles = (dir) => {
70
- const path = profilesFilePath(dir);
71
- if (!existsSync(path))
72
- return { kind: "absent" };
73
- const broken = (why) => ({
74
- kind: "unusable",
75
- reason: `${path} could not be read as a profiles file: ${why}`,
76
- });
77
- // Reading and parsing are separate failures with separate answers. Sharing one catch
78
- // reported `EACCES` as "not valid JSON", which sends the user to edit a file that is
79
- // perfectly valid and that they cannot open.
80
- let contents;
81
- try {
82
- contents = readFileSync(path, "utf8");
83
- }
84
- catch (err) {
85
- const code = err.code;
86
- return broken(code ? `reading it failed with ${code}` : "reading it failed");
87
- }
88
- let parsed;
89
- try {
90
- parsed = JSON.parse(contents);
91
- }
92
- catch {
93
- return broken("it is not valid JSON");
94
- }
95
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
96
- return broken("it does not contain an object");
97
- const profiles = parsed.profiles;
98
- if (profiles === null ||
99
- typeof profiles !== "object" ||
100
- Array.isArray(profiles))
101
- return broken("it has no `profiles` object");
102
- for (const [name, entry] of Object.entries(profiles)) {
103
- if (!NAME_PATTERN.test(name))
104
- return broken(`"${name}" is not a valid profile name`);
105
- if (entry === null ||
106
- typeof entry !== "object" ||
107
- typeof entry.credentials !== "string" ||
108
- entry.credentials.trim() === "") {
109
- return broken(`profile "${name}" has no \`credentials\` path`);
110
- }
111
- }
112
- return { kind: "ok", file: { version: 1, profiles } };
113
- };
114
- /**
115
- * Read `profiles.json`, or `null` when there is nothing usable there.
116
- *
117
- * A malformed file is reported through `onWarn` and treated as absent, because for a *read* the
118
- * worst case is a named profile turning up missing, which is recoverable — whereas throwing
119
- * would lock the user out of `neon auth` itself. Writing is the opposite: see
120
- * {@link upsertProfile}, which refuses rather than rebuilding a file it cannot read.
121
- */
122
- export const readProfiles = (dir,
123
- /** Called with the reason a profiles file was ignored. The consumer owns how it reports. */
124
- onWarn = () => { }) => {
125
- const read = inspectProfiles(dir);
126
- if (read.kind === "ok")
127
- return read.file;
128
- if (read.kind === "unusable")
129
- onWarn(read.reason);
130
- return null;
131
- };
132
- /**
133
- * Refuse to act on a named profile when the file that defines it cannot be read.
134
- *
135
- * Call this **before** anything that writes a credential, opens a browser, or spends an API
136
- * call. {@link upsertProfile} refuses too, but it runs last: by then `create` has already
137
- * overwritten `credentials.<name>.json` and revoked the key it replaced, and `neon auth
138
- * --profile` has already signed in over it — a refusal that arrives after the destruction it
139
- * exists to prevent. The path resolution itself is the unsound part, since with the metadata
140
- * unreadable the conventional filename is a guess about which account that file belongs to.
141
- *
142
- * `DEFAULT` is exempt: it is defined by the absence of metadata rather than by an entry, so
143
- * signing in normally must keep working while a broken `profiles.json` is repaired.
144
- */
145
- export const assertProfilesUsable = (dir, name) => {
146
- if (name === DEFAULT_PROFILE)
147
- return;
148
- const read = inspectProfiles(dir);
149
- if (read.kind === "unusable") {
150
- throw new Error(`${read.reason}. Fix or delete the file before working with profile "${name}" — it is the only record of where each account's credentials live.`);
151
- }
152
- };
153
- /** Resolve a profile to an absolute credentials path. Throws when a named profile is unknown. */
154
- export const resolveProfile = (dir, name) => {
155
- const read = inspectProfiles(dir);
156
- // A broken file must not be reported as `Unknown profile "work"`. That names the wrong
157
- // problem, and the user goes looking for a profile they can see in the file in front of them.
158
- if (read.kind === "unusable" && name !== DEFAULT_PROFILE) {
159
- throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
160
- }
161
- const file = read.kind === "ok" ? read.file : null;
162
- const entry = file?.profiles[name];
163
- if (entry) {
164
- return {
165
- name,
166
- credentialsPath: resolveEntryPath(dir, entry.credentials),
167
- ...(entry.label ? { label: entry.label } : {}),
168
- ...(entry.userId ? { userId: entry.userId } : {}),
169
- declared: true,
170
- };
171
- }
172
- // DEFAULT works with no profiles.json at all, and keeps working when one exists but
173
- // doesn't mention it — that is the pre-profiles behaviour, unchanged.
174
- if (name === DEFAULT_PROFILE) {
175
- return {
176
- name,
177
- credentialsPath: credentialsPath(dir),
178
- declared: false,
179
- };
180
- }
181
- const known = file
182
- ? Object.keys(file.profiles).join(", ")
183
- : DEFAULT_PROFILE;
184
- throw new Error(`Unknown profile "${name}". Known profiles: ${known}. Create it with \`neon profile create ${name}\`.`);
185
- };
186
- /** Default location for a new named profile's credentials file. */
187
- export const newProfileCredentialsPath = (dir, name) => resolve(dir, `credentials.${name}.json`);
188
- /**
189
- * Record a profile, creating `profiles.json` if this is the first named one.
190
- *
191
- * When the file is created, `DEFAULT` is written explicitly and pointed at wherever
192
- * `credentials.json` actually is. That matters for an install predating the directory
193
- * rename: `profiles.json` is created in `neon/` while the credentials are still in
194
- * `neonctl/`, so `DEFAULT` is recorded as `../neonctl/credentials.json` rather than a
195
- * relative name that would resolve to a file that isn't there.
196
- */
197
- export const upsertProfile = (dir, name, entry) => {
198
- assertValidProfileName(name);
199
- const path = profilesFilePath(dir);
200
- const read = inspectProfiles(dir);
201
- // Refusing is the point. Treating a broken file as absent here rebuilt it from a single
202
- // `DEFAULT` entry and dropped every named profile in it — silent data loss, in the file
203
- // that is the only record of where each account's credentials live. The credentials
204
- // themselves survive, so fixing the file by hand recovers everything.
205
- if (read.kind === "unusable") {
206
- throw new Error(`${read.reason}. Refusing to rewrite it, because doing so would discard the profiles it defines. Fix or delete the file, then re-run.`);
207
- }
208
- const file = read.kind === "ok"
209
- ? read.file
210
- : {
211
- version: 1,
212
- profiles: {
213
- [DEFAULT_PROFILE]: {
214
- credentials: relativeToProfiles(path, credentialsPath(dir)),
215
- },
216
- },
217
- };
218
- file.profiles[name] = {
219
- credentials: relativeToProfiles(path, entry.credentials),
220
- ...(entry.label ? { label: entry.label } : {}),
221
- ...(entry.userId ? { userId: entry.userId } : {}),
222
- };
223
- writeProfiles(path, file);
224
- };
225
- /** Remove an entry. Returns false when it wasn't there. */
226
- export const removeProfileEntry = (dir, name) => {
227
- const path = profilesFilePath(dir);
228
- const file = readProfiles(dir);
229
- if (!file?.profiles[name])
230
- return false;
231
- delete file.profiles[name];
232
- writeProfiles(path, file);
233
- return true;
234
- };
235
- /**
236
- * True when only `DEFAULT` is left, so `profiles.json` no longer earns its place. Mirrors
237
- * lazy creation: a single-account install has no profiles file, before or after.
238
- */
239
- export const onlyDefaultRemains = (file) => {
240
- const names = Object.keys(file.profiles);
241
- return (names.length === 0 ||
242
- (names.length === 1 && names[0] === DEFAULT_PROFILE));
243
- };
244
- export const listProfiles = (dir) => {
245
- const read = inspectProfiles(dir);
246
- // Listing is the command run to find out what is there, so a broken file is the answer
247
- // rather than an obstacle. Showing only `DEFAULT` would state, as fact, that the profiles
248
- // in that file do not exist.
249
- if (read.kind === "unusable") {
250
- throw new Error(`${read.reason}. Fix or delete the file — every named profile is defined in it.`);
251
- }
252
- const file = read.kind === "ok" ? read.file : null;
253
- if (!file)
254
- return [resolveProfile(dir, DEFAULT_PROFILE)];
255
- const names = Object.keys(file.profiles);
256
- if (!names.includes(DEFAULT_PROFILE))
257
- names.unshift(DEFAULT_PROFILE);
258
- return names.map((name) => resolveProfile(dir, name));
259
- };
260
- const writeProfiles = (path, file) => {
261
- writeSecretFile(path, `${JSON.stringify(file, null, 2)}\n`);
262
- };
263
- const resolveEntryPath = (dir, entry) => isAbsolute(entry) ? entry : resolve(profilesDir(dir), entry);
264
- /** `profiles.json` may sit in the legacy directory, so entries resolve against its own dir. */
265
- const profilesDir = (dir) => resolve(profilesFilePath(dir), "..");
266
- /** Keep entries relative when they sit near `profiles.json`; absolute paths stay absolute. */
267
- const relativeToProfiles = (profilesPath, target) => {
268
- const rel = relative(resolve(profilesPath, ".."), target);
269
- return rel && !isAbsolute(rel) ? rel : target;
270
- };
271
- function nonEmpty(value) {
272
- if (typeof value !== "string")
273
- return undefined;
274
- const trimmed = value.trim();
275
- return trimmed === "" ? undefined : trimmed;
276
- }