neon 2.47.0 → 3.1.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 (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +575 -658
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -64
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +133 -147
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +9 -7
package/README.md CHANGED
@@ -743,6 +743,59 @@ neon snapshots schedule set --branch main --schedule '[{"frequency":"weekly","da
743
743
 
744
744
  All sub-commands honor the [global options](#global-options), including `--output json|yaml|table`.
745
745
 
746
+ ## Logs (`logs`)
747
+
748
+ `neon logs` reads the log records the services on a branch emit — Neon Functions, object storage, and Postgres computes. **Logs require Neon Platform Beta and are currently available only for projects in `aws-us-east-2`.**
749
+
750
+ Every sub-command resolves the project through the standard chain (`--project-id`, then the `.neon` context file, then a single-project auto-detect), and takes `--branch <id|name>`, defaulting to the project's default branch. `logs query` searches the previous hour by default; `logs field-values` searches the previous six hours. The maximum time window is seven days.
751
+
752
+ ```bash
753
+ # The last 30 minutes on the default branch
754
+ neon logs query --since 30m
755
+
756
+ # Postgres compute errors on main, oldest first
757
+ neon logs query --branch main --source pg_endpoint --minimum-severity error --sort-order asc
758
+
759
+ # An explicit window (--start-time replaces --since; --end-time works with either)
760
+ neon logs query --start-time 2025-01-01T00:00:00Z --end-time 2025-01-01T01:00:00Z
761
+
762
+ # One request trace, across every service that took part in it
763
+ neon logs query --trace-id 4bf92f3577b34da6a3ce929d0e0e4736
764
+
765
+ # What the structured filters cannot express: a raw LogQL selection. It replaces
766
+ # them, but the window, --limit, --sort-order and --cursor still apply.
767
+ neon logs query --since 1h --logql '{entity_type="function"} |= "timeout"'
768
+
769
+ # Which fields this branch supports, and the values it has actually seen
770
+ neon logs fields
771
+ neon logs field-values service_name --since 6h --source function
772
+ ```
773
+
774
+ A response holds at most `--limit` records (1–1000; default 100). When more matched, table output prints the `--cursor` to repeat the same query with; `--output json|yaml` returns `is_truncated` and `next_cursor` on the envelope instead, so nothing but the payload lands on stdout. Table output shows the common fields; use structured output for the complete records:
775
+
776
+ ```console
777
+ $ neon logs query --since 24h --output json
778
+ {
779
+ "logs": [
780
+ {
781
+ "timestamp": "2025-01-01T00:00:02.000Z",
782
+ "message": "GET /api/todos 200",
783
+ "source": "function",
784
+ "service_name": "api",
785
+ "severity_text": "INFO",
786
+ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
787
+ "attributes": { "http_status": 200 }
788
+ }
789
+ ],
790
+ "next_cursor": "eyJvZmZzZXQiOjEwMH0",
791
+ "is_truncated": true
792
+ }
793
+ ```
794
+
795
+ Two combinations are rejected before the request: `--since` with `--start-time`, and `--logql` with any of `--source`, `--service-name`, `--scope-name`, `--minimum-severity`, `--severity-text`, `--body-contains` or `--trace-id`. `--minimum-severity` and `--severity-text` are independent filters and combine with AND. If Neon reports that `--minimum-severity` is unsupported, use `--severity-text` instead; `neon logs field-values severity_text` lists the exact values present on a branch.
796
+
797
+ `--body-contains` compares a case-sensitive substring against the rendered message. Structured bodies, including object storage records, are rendered as compact JSON, so match the JSON form (for example, `"http_status":200`).
798
+
746
799
  ## Profiles
747
800
 
748
801
  The CLI holds one Neon account by default. A profile adds another, and is nothing more than a pointer to a credentials file:
@@ -971,6 +1024,7 @@ API keys in org-7
971
1024
  | function | `deploy`, `list`, `get`, `delete` | Manage Neon Functions |
972
1025
  | [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
973
1026
  | [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
1027
+ | logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
974
1028
  | snapshots | `list`, `get`, `create`, `update`, `delete`, `restore`, `finalize`, `schedule get`, `schedule set` | Manage snapshots |
975
1029
  | [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
976
1030
  | [psql](https://neon.com/docs/reference/cli-psql) | | Connect to a database via psql |
@@ -1,86 +1,83 @@
1
+ import "./profiles.js";
2
+ //#region src/_shared/auth_selection.ts
1
3
  /**
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: "",
4
+ * # Which credential an invocation authenticates with
5
+ *
6
+ * Four inputs can each answer "who am I": the `--api-key` flag, `NEON_API_KEY`, the
7
+ * `--profile` flag, and `NEON_PROFILE`. This module decides between them, and it is pure so
8
+ * the decision can be tested without a filesystem, a network, or a config directory.
9
+ *
10
+ * ## The rule
11
+ *
12
+ * **An explicit flag beats an ambient environment variable.** That single rule fixes the bug
13
+ * this module exists for: before it, any API key — including one merely exported into the
14
+ * shell — silently voided `--profile`, so `neon --profile work …` would quietly run as
15
+ * whoever `NEON_API_KEY` belonged to and say nothing about it.
16
+ *
17
+ * | Given | What runs |
18
+ * | --- | --- |
19
+ * | `--api-key` and `--profile` | neither: contradictory explicit flags, so this throws |
20
+ * | `--api-key` and `NEON_PROFILE` | the flag's key |
21
+ * | `--profile` and `NEON_API_KEY` | the profile |
22
+ * | `NEON_API_KEY` and `NEON_PROFILE` | the key, and the ignored profile is named in a warning |
23
+ * | `--profile` or `NEON_PROFILE` alone | that profile |
24
+ * | nothing | `DEFAULT` |
25
+ *
26
+ * Two explicit flags throw rather than picking a winner. They express different intents —
27
+ * `--api-key` supplies a credential, `--profile` selects a stored one — so there is no
28
+ * reading of the command that makes both true, and guessing is how the original bug behaved.
29
+ *
30
+ * When both are merely ambient, the key wins. That keeps CI exactly as it was: a pipeline
31
+ * that injects `NEON_API_KEY` must not change behaviour because a `NEON_PROFILE` leaked into
32
+ * the environment. It warns instead of staying silent, because a disregarded account
33
+ * selection is precisely what nobody noticed last time.
34
+ *
35
+ * `auth` and the `profile` subcommands do not use any of this. They read the same flags with
36
+ * different meanings — `neon auth --profile work` names where to *write* a credential, and
37
+ * `neon profile create work --api-key …` names one to *store* — so their callers skip
38
+ * selection entirely rather than passing exemptions down here.
39
+ */
40
+ let inputs = {
41
+ apiKeyFlag: "",
42
+ apiKeyEnv: "",
43
+ profileEnv: ""
43
44
  };
44
- let inputs = NO_INPUTS;
45
- export const recordCredentialInputs = (recorded) => {
46
- inputs = recorded;
45
+ const recordCredentialInputs = (recorded) => {
46
+ inputs = recorded;
47
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
- };
48
+ const credentialInputs = () => inputs;
49
+ const selectCredential = ({ apiKeyFlag, profileFlag, apiKeyEnv, profileEnv }) => {
50
+ const flagKey = nonEmpty(apiKeyFlag);
51
+ const flagProfile = nonEmpty(profileFlag);
52
+ if (flagKey !== void 0 && flagProfile !== void 0) throw new Error("Pass either --api-key or --profile, not both. --api-key supplies a credential directly; --profile selects a stored one.");
53
+ if (flagKey !== void 0) return {
54
+ source: "explicit-api-key",
55
+ apiKey: flagKey
56
+ };
57
+ if (flagProfile !== void 0) return {
58
+ source: "profile",
59
+ profile: flagProfile,
60
+ explicit: true
61
+ };
62
+ const envKey = nonEmpty(apiKeyEnv);
63
+ const envProfile = nonEmpty(profileEnv);
64
+ if (envKey !== void 0) return {
65
+ source: "ambient-api-key",
66
+ apiKey: envKey,
67
+ ...envProfile !== void 0 ? { ignoredProfile: envProfile } : {}
68
+ };
69
+ return {
70
+ source: "profile",
71
+ profile: envProfile ?? "DEFAULT",
72
+ explicit: envProfile !== void 0
73
+ };
75
74
  };
76
75
  /** 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;
76
+ const displacedProfileWarning = (selection) => selection.source === "ambient-api-key" && selection.ignoredProfile !== void 0 ? `NEON_API_KEY is set, so profile "${selection.ignoredProfile}" from NEON_PROFILE was ignored. Pass --profile ${selection.ignoredProfile} to use it instead.` : null;
81
77
  function nonEmpty(value) {
82
- if (typeof value !== "string")
83
- return undefined;
84
- const trimmed = value.trim();
85
- return trimmed === "" ? undefined : trimmed;
78
+ if (typeof value !== "string") return void 0;
79
+ const trimmed = value.trim();
80
+ return trimmed === "" ? void 0 : trimmed;
86
81
  }
82
+ //#endregion
83
+ export { credentialInputs, displacedProfileWarning, recordCredentialInputs, selectCredential };
@@ -1,209 +1,188 @@
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
1
  import { writeSecretFile } from "./secure_file.js";
47
- export const OAUTH = "oauth";
48
- export const API_KEY = "api_key";
2
+ import { readFileSync } from "node:fs";
3
+ //#region src/_shared/credentials.ts
4
+ /**
5
+ * # Stored credentials — one file per account, two kinds
6
+ *
7
+ * A profile points at exactly one credentials file (see `./profiles.ts`), and that file says
8
+ * what kind of credential it holds. Adding API-key support this way rather than adding a
9
+ * second pointer to `profiles.json` keeps a profile what it already was — one name, one path
10
+ * — and means `profiles.json` needs no schema change at all.
11
+ *
12
+ * ```json
13
+ * // oauth: every file written before this existed. An absent `type` means this.
14
+ * { "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
15
+ *
16
+ * // api_key, stored by `neon profile create --api-key`
17
+ * { "type": "api_key", "api_key": "napi_…", "user_id": "…" }
18
+ *
19
+ * // api_key minted by `--mint --org-id`, which records the scope it was issued at
20
+ * { "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
21
+ * ```
22
+ *
23
+ * ## One profile, one kind
24
+ *
25
+ * A credentials file holds an API key or an OAuth session, never both, and `type` states
26
+ * which. An earlier draft let the two coexist — the idea being that a key could keep the
27
+ * session it was minted from and so rotate without a browser. It did not survive review, for
28
+ * two reasons that are worth recording so nobody rebuilds it:
29
+ *
30
+ * 1. **It never worked.** The resolver returned the key without testing it, so a revoked key
31
+ * failed to mint and never fell back to the session sitting beside it.
32
+ * 2. **It could mix accounts.** Nothing compared the identity of the credential being written
33
+ * with the one already there, so a profile could hold one account's session and another's
34
+ * key, told apart only by a single string. Flip or lose `type` and the profile silently
35
+ * becomes a different person.
36
+ *
37
+ * Recovery from a dead key is therefore one browser login — `neon profile create <name>
38
+ * --mint --force` — which is what the retained session was supposed to save and never did.
39
+ *
40
+ * ## Older releases
41
+ *
42
+ * A CLI predating this reads the pointer, finds no `type` it understands, ignores it, and
43
+ * looks for `access_token`. An `api_key` profile has none, so an older release falls through
44
+ * to its browser login rather than crashing. That it does not crash is why `credentials`
45
+ * stays a required pointer: an entry without one makes 2.41 and 2.42 throw
46
+ * `ERR_INVALID_ARG_TYPE` from `resolveEntryPath`.
47
+ */
48
+ const OAUTH = "oauth";
49
+ const API_KEY = "api_key";
49
50
  /**
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)}`);
51
+ * Which credential in this file authenticates, by declaration alone.
52
+ *
53
+ * An unrecognised `type` throws rather than falling back to `oauth`. A file we cannot
54
+ * interpret is a misconfiguration the user has to see: treating it as OAuth would send them
55
+ * to a browser login that silently replaces a credential they meant to keep, and treating it
56
+ * as an API key would authenticate with whatever `api_key` happened to be there.
57
+ *
58
+ * This deliberately does not check that an `api_key` file has a key — `neon profile list`
59
+ * needs the kind of a file it is not about to authenticate with, and must be able to report a
60
+ * broken one rather than throwing halfway through a table.
61
+ */
62
+ const credentialKind = (credentials, at) => {
63
+ const declared = credentials.type;
64
+ if (declared === void 0 || declared === "oauth") return OAUTH;
65
+ if (declared === "api_key") return API_KEY;
66
+ throw new Error(`${at.path} declares a "type" this version does not understand. Expected "${OAUTH}" or "${API_KEY}". ${repair(at)}`);
71
67
  };
72
68
  /**
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
- */
69
+ * The way out of a credentials file that cannot be read.
70
+ *
71
+ * One sentence, shared by every such error, because they all have the same two answers: write
72
+ * a new credential over it, or delete it and start again.
73
+ */
78
74
  const repair = (at) => `Replace it deliberately with \`neon profile create ${at.profile} --force\`, or delete the file.`;
79
75
  /**
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 };
76
+ * Resolve what to authenticate with, validating that the declared kind is actually usable.
77
+ *
78
+ * An `api_key` file with no key is a hard error rather than a fall-through to OAuth: the user
79
+ * asked for a key, and quietly opening a browser instead would replace the credential they
80
+ * were trying to fix.
81
+ */
82
+ const interpretCredentials = (credentials, at) => {
83
+ if (credentialKind(credentials, at) === "oauth") return { kind: OAUTH };
84
+ const apiKey = nonEmpty(credentials.api_key);
85
+ if (apiKey === void 0) throw new Error(`${at.path} declares "type": "${API_KEY}" but has no "api_key" value. ${repair(at)}`);
86
+ return {
87
+ kind: API_KEY,
88
+ apiKey
89
+ };
94
90
  };
95
91
  /**
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 };
92
+ * Read and classify a credentials file, without deciding what to do about it.
93
+ *
94
+ * A permission or I/O error still throws: there may be a perfectly good credential here that
95
+ * we cannot see, and treating that as absent would send the user to a browser login that
96
+ * overwrites it.
97
+ */
98
+ const inspectCredentials = (path) => {
99
+ let contents;
100
+ try {
101
+ contents = readFileSync(path, "utf8");
102
+ } catch (err) {
103
+ if (err.code === "ENOENT") return { kind: "absent" };
104
+ throw err;
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(contents);
109
+ } catch {
110
+ return {
111
+ kind: "unusable",
112
+ reason: `${path} is not valid JSON, so the credential in it cannot be read`
113
+ };
114
+ }
115
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {
116
+ kind: "unusable",
117
+ reason: `${path} does not contain a credentials object`
118
+ };
119
+ return {
120
+ kind: "ok",
121
+ credentials: parsed
122
+ };
136
123
  };
137
124
  /**
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;
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;
155
140
  };
156
- export const writeCredentials = (path, credentials) => {
157
- writeSecretFile(path, JSON.stringify(credentials));
141
+ const writeCredentials = (path, credentials) => {
142
+ writeSecretFile(path, JSON.stringify(credentials));
158
143
  };
159
144
  /**
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 } : {}),
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 } : {}
173
158
  });
174
159
  /** 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
- : {}),
160
+ const scopeOf = (credentials) => ({
161
+ ...typeof credentials.org_id === "string" ? { orgId: credentials.org_id } : {},
162
+ ...typeof credentials.project_id === "string" ? { projectId: credentials.project_id } : {}
182
163
  });
183
164
  /** 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";
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";
190
169
  };
191
170
  function nonEmpty(value) {
192
- if (typeof value !== "string")
193
- return undefined;
194
- const trimmed = value.trim();
195
- return trimmed === "" ? undefined : trimmed;
171
+ if (typeof value !== "string") return void 0;
172
+ const trimmed = value.trim();
173
+ return trimmed === "" ? void 0 : trimmed;
196
174
  }
197
175
  /**
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();
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();
209
186
  };
187
+ //#endregion
188
+ export { API_KEY, OAUTH, apiKeyCredentials, credentialKind, describeScope, inspectCredentials, interpretCredentials, isSameCredential, readCredentials, scopeOf, writeCredentials };