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
package/dist/writer.js CHANGED
@@ -1,97 +1,77 @@
1
- import chalk from "chalk";
2
- import Table from "cli-table";
3
- import YAML from "yaml";
4
1
  import { isCi } from "./env.js";
5
2
  import { isObject, toSnakeCase } from "./utils/string.js";
3
+ import chalk from "chalk";
4
+ import YAML from "yaml";
5
+ import Table from "cli-table";
6
+ //#region src/writer.ts
6
7
  const writeYaml = (chunks) => {
7
- return YAML.stringify(chunks.length === 1
8
- ? chunks[0].data
9
- : Object.fromEntries(chunks.map(({ config, data }, idx) => [
10
- config.title ? toSnakeCase(config.title) : idx,
11
- data,
12
- ])), null, 2);
8
+ return YAML.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
13
9
  };
14
10
  const writeJson = (chunks) => {
15
- return JSON.stringify(chunks.length === 1
16
- ? chunks[0].data
17
- : Object.fromEntries(chunks.map(({ config, data }, idx) => [
18
- config.title ? toSnakeCase(config.title) : idx,
19
- data,
20
- ])), null, 2);
11
+ return JSON.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
21
12
  };
22
13
  const writeTable = (chunks, out) => {
23
- chunks.forEach(({ data, config: { emptyMessage, fields, title, renderColumns = {} }, }) => {
24
- const arrayData = Array.isArray(data) ? data : [data];
25
- if (!arrayData.length && emptyMessage) {
26
- out.write("\n" + emptyMessage + "\n");
27
- return;
28
- }
29
- const fieldsFiltered = fields.filter((field) => arrayData.some((item) => item[field] !== undefined && item[field] !== ""));
30
- const table = new Table({
31
- style: {
32
- head: ["green"],
33
- },
34
- head: fieldsFiltered.map((field) => field
35
- .split("_")
36
- .map((word) => word[0].toUpperCase() + word.slice(1))
37
- .join(" ")),
38
- });
39
- arrayData.forEach((item) => {
40
- table.push(fieldsFiltered.map((field) => {
41
- const value = item[field];
42
- if (renderColumns[field]) {
43
- return renderColumns[field]?.(item);
44
- }
45
- return Array.isArray(value)
46
- ? value.join("\n")
47
- : isObject(value)
48
- ? JSON.stringify(value, null, 2)
49
- : (value ?? "");
50
- }));
51
- });
52
- if (title) {
53
- out.write((isCi() ? title : chalk.bold(title)) + "\n");
54
- }
55
- out.write(table.toString());
56
- out.write("\n");
57
- });
14
+ chunks.forEach(({ data, config: { emptyMessage, fields, title, renderColumns = {} } }) => {
15
+ const arrayData = Array.isArray(data) ? data : [data];
16
+ if (!arrayData.length && emptyMessage) {
17
+ out.write("\n" + emptyMessage + "\n");
18
+ return;
19
+ }
20
+ const fieldsFiltered = fields.filter((field) => arrayData.some((item) => item[field] !== void 0 && item[field] !== ""));
21
+ const table = new Table({
22
+ style: { head: ["green"] },
23
+ head: fieldsFiltered.map((field) => field.split("_").map((word) => word[0].toUpperCase() + word.slice(1)).join(" "))
24
+ });
25
+ arrayData.forEach((item) => {
26
+ table.push(fieldsFiltered.map((field) => {
27
+ const value = item[field];
28
+ if (renderColumns[field]) return renderColumns[field]?.(item);
29
+ return Array.isArray(value) ? value.join("\n") : isObject(value) ? JSON.stringify(value, null, 2) : value ?? "";
30
+ }));
31
+ });
32
+ if (title) out.write((isCi() ? title : chalk.bold(title)) + "\n");
33
+ out.write(table.toString());
34
+ out.write("\n");
35
+ });
58
36
  };
59
37
  /**
60
- *
61
- * Parses the output format, takes data and writes the output to stdout.
62
- *
63
- * @example
64
- * const { data } = await props.apiClient.listProjectBranches(props.project.id);
65
- * // to output single data
66
- * writer(props).end(data, { fields: ['id', 'name', 'created_at'] })
67
- * // to output multiple data
68
- * writer(props)
69
- * .write(data, { fields: ['id', 'name', 'created_at'], title: 'branches' })
70
- * .write(data, { fields: ['id', 'created_at'], title: 'endpoints' })
71
- * .end()
72
- */
73
- export const writer = (props) => {
74
- const out = props.out ?? process.stdout;
75
- const chunks = [];
76
- return {
77
- write(data, config) {
78
- chunks.push({ data, config });
79
- return this;
80
- },
81
- text(data) {
82
- return out.write(data);
83
- },
84
- end: (...args) => {
85
- if (args.length === 2) {
86
- chunks.push({ data: args[0], config: args[1] });
87
- }
88
- if (props.output == "yaml") {
89
- return out.write(writeYaml(chunks));
90
- }
91
- if (props.output == "json") {
92
- return out.write(writeJson(chunks));
93
- }
94
- writeTable(chunks, out);
95
- },
96
- };
38
+ *
39
+ * Parses the output format, takes data and writes the output to stdout.
40
+ *
41
+ * @example
42
+ * const { data } = await props.apiClient.listProjectBranches(props.project.id);
43
+ * // to output single data
44
+ * writer(props).end(data, { fields: ['id', 'name', 'created_at'] })
45
+ * // to output multiple data
46
+ * writer(props)
47
+ * .write(data, { fields: ['id', 'name', 'created_at'], title: 'branches' })
48
+ * .write(data, { fields: ['id', 'created_at'], title: 'endpoints' })
49
+ * .end()
50
+ */
51
+ const writer = (props) => {
52
+ const out = props.out ?? process.stdout;
53
+ const chunks = [];
54
+ return {
55
+ write(data, config) {
56
+ chunks.push({
57
+ data,
58
+ config
59
+ });
60
+ return this;
61
+ },
62
+ text(data) {
63
+ return out.write(data);
64
+ },
65
+ end: (...args) => {
66
+ if (args.length === 2) chunks.push({
67
+ data: args[0],
68
+ config: args[1]
69
+ });
70
+ if (props.output == "yaml") return out.write(writeYaml(chunks));
71
+ if (props.output == "json") return out.write(writeJson(chunks));
72
+ writeTable(chunks, out);
73
+ }
74
+ };
97
75
  };
76
+ //#endregion
77
+ export { writer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -25,6 +25,8 @@
25
25
  ".": "./dist/index.js",
26
26
  "./cli": "./dist/cli.js",
27
27
  "./dist/_shared/*": null,
28
+ "./dist/_chunks/*": null,
29
+ "./dist/_virtual/*": null,
28
30
  "./dist/*": "./dist/*",
29
31
  "./package.json": "./package.json"
30
32
  },
@@ -61,9 +63,9 @@
61
63
  "yaml": "^2.9.0",
62
64
  "yargs": "17.7.2",
63
65
  "yoctocolors": "^2.1.2",
64
- "@neon/config-runtime": "1.0.0",
65
66
  "@neon/sdk": "2.0.0",
66
- "@neon/config": "1.0.0"
67
+ "@neon/config": "1.0.0",
68
+ "@neon/config-runtime": "1.0.0"
67
69
  },
68
70
  "optionalDependencies": {
69
71
  "esbuild": "0.28.1"
@@ -90,9 +92,12 @@
90
92
  "openapi-types": "12.1.3",
91
93
  "rollup": "3.29.4",
92
94
  "strip-ansi": "7.1.0",
95
+ "tsdown": "^0.14.1",
93
96
  "tsx": "4.22.3",
94
97
  "typescript": "^5.9.0",
95
98
  "vitest": "^3.0.9",
99
+ "@neon-internals/cli-core": "0.0.0",
100
+ "@neon-internals/env-core": "0.0.0",
96
101
  "@neon/e2e-harness": "0.0.0"
97
102
  },
98
103
  "publishConfig": {
@@ -115,10 +120,10 @@
115
120
  },
116
121
  "scripts": {
117
122
  "generateParams": "tsx generateOptionsFromSpec.ts",
118
- "codegen": "node ../../scripts/sync-shared.mjs . && node scripts/set-vsx-gallery.mjs",
123
+ "codegen": "node scripts/set-vsx-gallery.mjs",
119
124
  "clean": "rm -rf dist",
120
- "build": "pnpm codegen && pnpm generateParams && pnpm clean && tsc -p tsconfig.build.json && cp src/*.html ./dist",
121
- "build:internal": "node ../../scripts/sync-shared.mjs . && node scripts/set-vsx-gallery.mjs https://cursor-vsx-proxy.cloud.databricks.com/gallery && pnpm generateParams && pnpm clean && tsc -p tsconfig.build.json && cp src/*.html ./dist",
125
+ "build": "pnpm codegen && pnpm generateParams && tsc --noEmit && tsdown && cp src/*.html ./dist",
126
+ "build:internal": "node scripts/set-vsx-gallery.mjs https://cursor-vsx-proxy.cloud.databricks.com/gallery && pnpm generateParams && tsc --noEmit && tsdown && cp src/*.html ./dist",
122
127
  "bundle": "node pkg.js",
123
128
  "typecheck": "pnpm codegen && tsc --noEmit",
124
129
  "lint": "pnpm codegen && pnpm typecheck && biome check src",
@@ -1,86 +0,0 @@
1
- /**
2
- * # Which credential an invocation authenticates with
3
- *
4
- * Four inputs can each answer "who am I": the `--api-key` flag, `NEON_API_KEY`, the
5
- * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so
6
- * the decision can be tested without a filesystem, a network, or a config directory.
7
- *
8
- * ## The rule
9
- *
10
- * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug
11
- * this module exists for: before it, any API key — including one merely exported into the
12
- * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as
13
- * whoever `NEON_API_KEY` belonged to and say nothing about it.
14
- *
15
- * | Given | What runs |
16
- * | --- | --- |
17
- * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |
18
- * | `--api-key` and `NEON_PROFILE` | the flag's key |
19
- * | `--profile` and `NEON_API_KEY` | the profile |
20
- * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |
21
- * | `--profile` or `NEON_PROFILE` alone | that profile |
22
- * | nothing | `DEFAULT` |
23
- *
24
- * Two explicit flags throw rather than picking a winner. They express different intents —
25
- * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no
26
- * reading of the command that makes both true, and guessing is how the original bug behaved.
27
- *
28
- * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline
29
- * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into
30
- * the environment. It warns instead of staying silent, because a disregarded account
31
- * selection is precisely what nobody noticed last time.
32
- *
33
- * `auth` and the `profile` subcommands do not use any of this. They read the same flags with
34
- * different meanings — `neon auth --profile work` names where to *write* a credential, and
35
- * `neon profile create work --api-key …` names one to *store* — so their callers skip
36
- * selection entirely rather than passing exemptions down here.
37
- */
38
- import { DEFAULT_PROFILE } from "./profiles.js";
39
- const NO_INPUTS = {
40
- apiKeyFlag: "",
41
- apiKeyEnv: "",
42
- profileEnv: "",
43
- };
44
- let inputs = NO_INPUTS;
45
- export const recordCredentialInputs = (recorded) => {
46
- inputs = recorded;
47
- };
48
- export const credentialInputs = () => inputs;
49
- export const selectCredential = ({ apiKeyFlag, profileFlag, apiKeyEnv, profileEnv, }) => {
50
- const flagKey = nonEmpty(apiKeyFlag);
51
- const flagProfile = nonEmpty(profileFlag);
52
- if (flagKey !== undefined && flagProfile !== undefined) {
53
- throw new Error("Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.");
54
- }
55
- if (flagKey !== undefined) {
56
- return { source: "explicit-api-key", apiKey: flagKey };
57
- }
58
- if (flagProfile !== undefined) {
59
- return { source: "profile", profile: flagProfile, explicit: true };
60
- }
61
- const envKey = nonEmpty(apiKeyEnv);
62
- const envProfile = nonEmpty(profileEnv);
63
- if (envKey !== undefined) {
64
- return {
65
- source: "ambient-api-key",
66
- apiKey: envKey,
67
- ...(envProfile !== undefined ? { ignoredProfile: envProfile } : {}),
68
- };
69
- }
70
- return {
71
- source: "profile",
72
- profile: envProfile ?? DEFAULT_PROFILE,
73
- explicit: envProfile !== undefined,
74
- };
75
- };
76
- /** The warning for an ambient key that displaced an ambient profile, or `null`. */
77
- export const displacedProfileWarning = (selection) => selection.source === "ambient-api-key" &&
78
- selection.ignoredProfile !== undefined
79
- ? `NEON_API_KEY is set, so profile "${selection.ignoredProfile}" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.`
80
- : null;
81
- function nonEmpty(value) {
82
- if (typeof value !== "string")
83
- return undefined;
84
- const trimmed = value.trim();
85
- return trimmed === "" ? undefined : trimmed;
86
- }
@@ -1,209 +0,0 @@
1
- /**
2
- * # Stored credentials — one file per account, two kinds
3
- *
4
- * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says
5
- * what kind of credential it holds. Adding API-key support this way rather than adding a
6
- * second pointer to `profiles.json` keeps a profile what it already was — one name, one path
7
- * — and means `profiles.json` needs no schema change at all.
8
- *
9
- * ```json
10
- * // oauth: every file written before this existed. An absent `type` means this.
11
- * { "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
12
- *
13
- * // api_key, stored by `neon profile create --api-key`
14
- * { "type": "api_key", "api_key": "napi_…", "user_id": "…" }
15
- *
16
- * // api_key minted by `--mint --org-id`, which records the scope it was issued at
17
- * { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
18
- * ```
19
- *
20
- * ## One profile, one kind
21
- *
22
- * A credentials file holds an API key or an OAuth session, never both, and `type` states
23
- * which. An earlier draft let the two coexist — the idea being that a key could keep the
24
- * session it was minted from and so rotate without a browser. It did not survive review, for
25
- * two reasons that are worth recording so nobody rebuilds it:
26
- *
27
- * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key
28
- * failed to mint and never fell back to the session sitting beside it.
29
- * 2. **It could mix accounts.** Nothing compared the identity of the credential being written
30
- * with the one already there, so a profile could hold one account's session and another's
31
- * key, told apart only by a single string. Flip or lose `type` and the profile silently
32
- * becomes a different person.
33
- *
34
- * Recovery from a dead key is therefore one browser login — `neon profile create <name>
35
- * --mint --force` — which is what the retained session was supposed to save and never did.
36
- *
37
- * ## Older releases
38
- *
39
- * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and
40
- * looks for `access_token`. An `api_key` profile has none, so an older release falls through
41
- * to its browser login rather than crashing. That it does not crash is why `credentials`
42
- * stays a required pointer: an entry without one makes 2.41 and 2.42 throw
43
- * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.
44
- */
45
- import { readFileSync } from "node:fs";
46
- import { writeSecretFile } from "./secure_file.js";
47
- export const OAUTH = "oauth";
48
- export const API_KEY = "api_key";
49
- /**
50
- * Which credential in this file authenticates, by declaration alone.
51
- *
52
- * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot
53
- * interpret is a misconfiguration the user has to see: treating it as OAuth would send them
54
- * to a browser login that silently replaces a credential they meant to keep, and treating it
55
- * as an API key would authenticate with whatever `api_key` happened to be there.
56
- *
57
- * This deliberately does not check that an `api_key` file has a key — `neon profile list`
58
- * needs the kind of a file it is not about to authenticate with, and must be able to report a
59
- * broken one rather than throwing halfway through a table.
60
- */
61
- export const credentialKind = (credentials, at) => {
62
- const declared = credentials.type;
63
- if (declared === undefined || declared === OAUTH)
64
- return OAUTH;
65
- if (declared === API_KEY)
66
- return API_KEY;
67
- // The value is not quoted back. Everything in this file is secret material, and a
68
- // corrupted or hand-edited file can put a key anywhere in it — including here. Naming the
69
- // file is enough to act on, and it cannot leak what the file holds.
70
- throw new Error(`${at.path} declares a "type" this version does not understand. Expected "${OAUTH}" or "${API_KEY}". ${repair(at)}`);
71
- };
72
- /**
73
- * The way out of a credentials file that cannot be read.
74
- *
75
- * One sentence, shared by every such error, because they all have the same two answers: write
76
- * a new credential over it, or delete it and start again.
77
- */
78
- const repair = (at) => `Replace it deliberately with \`neon profile create ${at.profile} --force\`, or delete the file.`;
79
- /**
80
- * Resolve what to authenticate with, validating that the declared kind is actually usable.
81
- *
82
- * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user
83
- * asked for a key, and quietly opening a browser instead would replace the credential they
84
- * were trying to fix.
85
- */
86
- export const interpretCredentials = (credentials, at) => {
87
- if (credentialKind(credentials, at) === OAUTH)
88
- return { kind: OAUTH };
89
- const apiKey = nonEmpty(credentials.api_key);
90
- if (apiKey === undefined) {
91
- throw new Error(`${at.path} declares "type": "${API_KEY}" but has no "api_key" value. ${repair(at)}`);
92
- }
93
- return { kind: API_KEY, apiKey };
94
- };
95
- /**
96
- * Read and classify a credentials file, without deciding what to do about it.
97
- *
98
- * A permission or I/O error still throws: there may be a perfectly good credential here that
99
- * we cannot see, and treating that as absent would send the user to a browser login that
100
- * overwrites it.
101
- */
102
- export const inspectCredentials = (path) => {
103
- let contents;
104
- try {
105
- contents = readFileSync(path, "utf8");
106
- }
107
- catch (err) {
108
- if (err.code === "ENOENT")
109
- return { kind: "absent" };
110
- throw err;
111
- }
112
- let parsed;
113
- try {
114
- parsed = JSON.parse(contents);
115
- }
116
- catch {
117
- // The parser's message is deliberately discarded. V8 quotes a window of the input
118
- // around the syntax error — on Node 24 a truncated credentials file produced
119
- // `Unexpected token 'a', ..."api_key":napi_SUPERS"... is not valid JSON` — and this
120
- // reason is printed by `profile list` and by every failed authentication. A malformed
121
- // secret file is exactly when a diagnostic must say less, not more.
122
- return {
123
- kind: "unusable",
124
- reason: `${path} is not valid JSON, so the credential in it cannot be read`,
125
- };
126
- }
127
- if (parsed === null ||
128
- typeof parsed !== "object" ||
129
- Array.isArray(parsed)) {
130
- return {
131
- kind: "unusable",
132
- reason: `${path} does not contain a credentials object`,
133
- };
134
- }
135
- return { kind: "ok", credentials: parsed };
136
- };
137
- /**
138
- * The credential at `path`, or `null` when the file is not there.
139
- *
140
- * A damaged file is an error, not an absence. Treating it as absent — which is what this used to
141
- * do — meant any read-only command could repair it by starting a browser sign-in and overwriting
142
- * it, **possibly as a different account**, with the user never having asked for a repair and no
143
- * way back to whatever was in the file. Failing here costs one deliberate command; the message
144
- * names it.
145
- *
146
- * `profile list` and telemetry use {@link inspectCredentials} instead, because describing a
147
- * broken credential is not the same as using one.
148
- */
149
- export const readCredentials = (at) => {
150
- const read = inspectCredentials(at.path);
151
- if (read.kind === "unusable") {
152
- throw new Error(`${read.reason}. ${repair(at)}`);
153
- }
154
- return read.kind === "ok" ? read.credentials : null;
155
- };
156
- export const writeCredentials = (path, credentials) => {
157
- writeSecretFile(path, JSON.stringify(credentials));
158
- };
159
- /**
160
- * Build an `api_key` credentials object. Nothing from a previous credential is carried over.
161
- *
162
- * The scope is stored because it is not recoverable from the secret: `rotate-key` has to mint
163
- * the replacement on the same endpoint, and an org or project key minted as an account key
164
- * would silently widen what the profile reaches.
165
- */
166
- export const apiKeyCredentials = ({ apiKey, keyId, userId, scope, }) => ({
167
- type: API_KEY,
168
- api_key: apiKey,
169
- ...(keyId !== undefined ? { key_id: keyId } : {}),
170
- ...(userId !== undefined ? { user_id: userId } : {}),
171
- ...(scope?.orgId !== undefined ? { org_id: scope.orgId } : {}),
172
- ...(scope?.projectId !== undefined ? { project_id: scope.projectId } : {}),
173
- });
174
- /** The scope recorded on a stored credential. */
175
- export const scopeOf = (credentials) => ({
176
- ...(typeof credentials.org_id === "string"
177
- ? { orgId: credentials.org_id }
178
- : {}),
179
- ...(typeof credentials.project_id === "string"
180
- ? { projectId: credentials.project_id }
181
- : {}),
182
- });
183
- /** How to describe a scope in output. */
184
- export const describeScope = (scope) => {
185
- if (scope.projectId !== undefined)
186
- return `project ${scope.projectId}`;
187
- if (scope.orgId !== undefined)
188
- return `org ${scope.orgId}`;
189
- return "account";
190
- };
191
- function nonEmpty(value) {
192
- if (typeof value !== "string")
193
- return undefined;
194
- const trimmed = value.trim();
195
- return trimmed === "" ? undefined : trimmed;
196
- }
197
- /**
198
- * Whether a stored credential is the same secret as the one about to replace it.
199
- *
200
- * Re-storing the key a profile already holds is a no-op, not a replacement — and retiring it
201
- * would revoke the credential the command has just committed to. Trimmed on both sides, because
202
- * a key read from a file or a pipe arrives with a trailing newline.
203
- */
204
- export const isSameCredential = (existingKey, replacementKey) => {
205
- if (existingKey === undefined || replacementKey === undefined)
206
- return false;
207
- const trimmed = existingKey.trim();
208
- return trimmed !== "" && trimmed === replacementKey.trim();
209
- };