neon 3.0.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 +354 -517
  5. package/dist/_shared/env-core/reuse-secrets.js +159 -203
  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 +573 -690
  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 +639 -719
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -355
  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 -68
  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 +130 -189
  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 +7 -5
@@ -1,558 +1,395 @@
1
+ import { ErrorCode, PlatformError, createNeonApiFromOptions, deriveCredentialScopes, resolveConfig } from "@neon/config/v1";
2
+ //#region src/_shared/env-core/env.ts
1
3
  /**
2
- * The Neon env core — resolving a branch's env from the Neon API, and projecting it into
3
- * OS-level `{ KEY: value }` pairs.
4
- *
5
- * Shared source, not a package: `scripts/sync-shared.mjs` copies it into `@neon/env` (which
6
- * publishes it as `fetchEnv` / `toEntries`) and into the `neon` CLI (which needs the
7
- * credential-reuse half in `reuse-secrets.ts`). See `shared/env-core/README.md` for why it is
8
- * a copy rather than an import.
9
- *
10
- * The counterpart that reads `process.env` — `parseEnv` and its zod schemas — is not here. It
11
- * has no consumer outside `@neon/env`, so it stays in that package and imports this.
12
- */
13
- import { createNeonApiFromOptions, deriveCredentialScopes, ErrorCode, PlatformError, resolveConfig, } from "@neon/config/v1";
4
+ * The Neon env core — resolving a branch's env from the Neon API, and projecting it into
5
+ * OS-level `{ KEY: value }` pairs.
6
+ *
7
+ * Shared source, not a package: `scripts/sync-shared.mjs` copies it into `@neon/env` (which
8
+ * publishes it as `fetchEnv` / `toEntries`) and into the `neon` CLI (which needs the
9
+ * credential-reuse half in `reuse-secrets.ts`). See `shared/env-core/README.md` for why it is
10
+ * a copy rather than an import.
11
+ *
12
+ * The counterpart that reads `process.env` — `parseEnv` and its zod schemas — is not here. It
13
+ * has no consumer outside `@neon/env`, so it stays in that package and imports this.
14
+ */
14
15
  /**
15
- * Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
16
- * for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
17
- * that talks to `process.env`).
18
- *
19
- * Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
20
- * camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
21
- * by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
22
- * {@link dataApiEnvSchema}.
23
- */
16
+ * Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
17
+ * for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
18
+ * that talks to `process.env`).
19
+ *
20
+ * Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
21
+ * camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
22
+ * by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
23
+ * {@link dataApiEnvSchema}.
24
+ */
24
25
  /**
25
- * Neon's default branch owner role, created with every project. This is the role a
26
- * `DATABASE_URL` should connect as.
27
- */
26
+ * Neon's default branch owner role, created with every project. This is the role a
27
+ * `DATABASE_URL` should connect as.
28
+ */
28
29
  const NEON_DEFAULT_OWNER_ROLE = "neondb_owner";
29
30
  /**
30
- * Neon's default database, created with every project. When a branch has several databases
31
- * and none was requested, this is preferred for the `DATABASE_URL` so the common case (a
32
- * user added a second database next to `neondb`) auto-picks without asking.
33
- */
31
+ * Neon's default database, created with every project. When a branch has several databases
32
+ * and none was requested, this is preferred for the `DATABASE_URL` so the common case (a
33
+ * user added a second database next to `neondb`) auto-picks without asking.
34
+ */
34
35
  const NEON_DEFAULT_DATABASE = "neondb";
35
36
  /**
36
- * Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
37
- * RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
38
- * so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
39
- * Data API (`neon config apply`) adds these next to the owner role, which is why a plain
40
- * branch routinely reports more than one role.
41
- */
42
- const NEON_MANAGED_AUTH_ROLES = new Set([
43
- "authenticator",
44
- "anonymous",
45
- "authenticated",
37
+ * Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
38
+ * RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
39
+ * so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
40
+ * Data API (`neon config apply`) adds these next to the owner role, which is why a plain
41
+ * branch routinely reports more than one role.
42
+ */
43
+ const NEON_MANAGED_AUTH_ROLES = /* @__PURE__ */ new Set([
44
+ "authenticator",
45
+ "anonymous",
46
+ "authenticated"
46
47
  ]);
47
- export const NEON_ENV_VAR_KEYS = {
48
- /**
49
- * Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
50
- * Neon Functions runtime on every branch (including the default) by default. `env pull` /
51
- * `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
52
- */
53
- branch: {
54
- name: "NEON_BRANCH",
55
- },
56
- postgres: {
57
- databaseUrl: "DATABASE_URL",
58
- databaseUrlUnpooled: "DATABASE_URL_UNPOOLED",
59
- },
60
- auth: {
61
- baseUrl: "NEON_AUTH_BASE_URL",
62
- jwksUrl: "NEON_AUTH_JWKS_URL",
63
- },
64
- dataApi: {
65
- url: "NEON_DATA_API_URL",
66
- },
67
- /**
68
- * Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
69
- * a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
70
- * `region` is injected under the SDK-standard `AWS_REGION`.
71
- */
72
- storage: {
73
- accessKeyId: "AWS_ACCESS_KEY_ID",
74
- secretAccessKey: "AWS_SECRET_ACCESS_KEY",
75
- endpoint: "AWS_ENDPOINT_URL_S3",
76
- region: "AWS_REGION",
77
- },
78
- /**
79
- * AI Gateway (Preview). Exposed under the Neon-branded env vars the deployed Functions
80
- * runtime injects: `apiKey` is the minted credential's bearer (`NEON_AI_GATEWAY_TOKEN`)
81
- * and `baseUrl` is the bare branch gateway host (`NEON_AI_GATEWAY_BASE_URL`,
82
- * `scheme://host`, no path). Clients like `@neon/ai-sdk-provider` read these and append the
83
- * dialect route (`/v1`, `/openai/v1`, `/anthropic/v1`) themselves (https://github.com/vercel/ai/pull/15997).
84
- */
85
- aiGateway: {
86
- apiKey: "NEON_AI_GATEWAY_TOKEN",
87
- baseUrl: "NEON_AI_GATEWAY_BASE_URL",
88
- },
48
+ const NEON_ENV_VAR_KEYS = {
49
+ /**
50
+ * Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
51
+ * Neon Functions runtime on every branch (including the default) by default. `env pull` /
52
+ * `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
53
+ */
54
+ branch: { name: "NEON_BRANCH" },
55
+ postgres: {
56
+ databaseUrl: "DATABASE_URL",
57
+ databaseUrlUnpooled: "DATABASE_URL_UNPOOLED"
58
+ },
59
+ auth: {
60
+ baseUrl: "NEON_AUTH_BASE_URL",
61
+ jwksUrl: "NEON_AUTH_JWKS_URL"
62
+ },
63
+ dataApi: { url: "NEON_DATA_API_URL" },
64
+ /**
65
+ * Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
66
+ * a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
67
+ * `region` is injected under the SDK-standard `AWS_REGION`.
68
+ */
69
+ storage: {
70
+ accessKeyId: "AWS_ACCESS_KEY_ID",
71
+ secretAccessKey: "AWS_SECRET_ACCESS_KEY",
72
+ endpoint: "AWS_ENDPOINT_URL_S3",
73
+ region: "AWS_REGION"
74
+ },
75
+ /**
76
+ * AI Gateway (Preview). Exposed under the Neon-branded env vars the deployed Functions
77
+ * runtime injects: `apiKey` is the minted credential's bearer (`NEON_AI_GATEWAY_TOKEN`)
78
+ * and `baseUrl` is the bare branch gateway host (`NEON_AI_GATEWAY_BASE_URL`,
79
+ * `scheme://host`, no path). Clients like `@neon/ai-sdk-provider` read these and append the
80
+ * dialect route (`/v1`, `/openai/v1`, `/anthropic/v1`) themselves (https://github.com/vercel/ai/pull/15997).
81
+ */
82
+ aiGateway: {
83
+ apiKey: "NEON_AI_GATEWAY_TOKEN",
84
+ baseUrl: "NEON_AI_GATEWAY_BASE_URL"
85
+ }
89
86
  };
90
- export async function fetchEnv(config, options) {
91
- return fetchEnvKeys(config, options, options.keys ?? null);
87
+ async function fetchEnv(config, options) {
88
+ return fetchEnvKeys(config, options, options.keys ?? null);
92
89
  }
93
90
  /**
94
- * The {@link fetchEnv} body, with the key selection as a plain argument and no generic
95
- * narrowing. Exists for callers that compute the selection at runtime — notably
96
- * {@link fetchEnvReusingSecrets}, which decides which keys it still needs by checking the
97
- * branch — since the public overload's `keys` is bound to a literal union those callers cannot
98
- * produce without asserting.
99
- *
100
- * `keys === null` selects everything the policy enables.
101
- */
102
- export async function fetchEnvKeys(config, options, keys) {
103
- const api = options.api ?? createApiFromOptions(options);
104
- const projectId = options.projectId;
105
- const { branch, desired } = await resolveBranchPolicy(config, options, api);
106
- const selection = keys ? new Set(keys) : null;
107
- const wants = (key) => selection === null || selection.has(key);
108
- const result = {};
109
- const [roles, databases] = await Promise.all([
110
- api.listBranchRoles(projectId, branch.id),
111
- api.listBranchDatabases(projectId, branch.id),
112
- ]);
113
- const roleName = pickRoleName(roles, branch, options.roleName);
114
- const databaseName = pickDatabaseName(databases, branch, options.databaseName);
115
- // Fan out: always fetch both Postgres URIs — the direct one also derives the AI Gateway
116
- // host, so a selection that drops `DATABASE_URL_UNPOOLED` still needs it. Conditionally
117
- // fetch auth + dataApi based on the branch policy and the selection. Auth key fields are
118
- // only returned at integration creation time; for Better Auth they may legitimately be
119
- // empty, so they can come back as empty strings.
120
- const K = NEON_ENV_VAR_KEYS;
121
- const wantsAuth = desired.authEnabled && (wants(K.auth.baseUrl) || wants(K.auth.jwksUrl));
122
- const wantsDataApi = desired.dataApiEnabled && wants(K.dataApi.url);
123
- const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
124
- api.getConnectionUri(projectId, {
125
- branchId: branch.id,
126
- databaseName,
127
- roleName,
128
- pooled: true,
129
- }),
130
- api.getConnectionUri(projectId, {
131
- branchId: branch.id,
132
- databaseName,
133
- roleName,
134
- pooled: false,
135
- }),
136
- wantsAuth
137
- ? api.getNeonAuth(projectId, branch.id)
138
- : Promise.resolve(null),
139
- wantsDataApi
140
- ? api.getNeonDataApi(projectId, branch.id, databaseName)
141
- : Promise.resolve(null),
142
- ]);
143
- const postgres = {};
144
- if (wants(K.postgres.databaseUrl))
145
- postgres.databaseUrl = pooled.uri;
146
- if (wants(K.postgres.databaseUrlUnpooled)) {
147
- postgres.databaseUrlUnpooled = unpooled.uri;
148
- }
149
- if (Object.keys(postgres).length > 0)
150
- result.postgres = postgres;
151
- // Branch identity, mirroring what the Functions runtime injects on every branch. Surfaced
152
- // as `NEON_BRANCH` so local dev (`neon dev` / `neon-env run` / `env pull`) matches the
153
- // deployed runtime. Uses the branch name.
154
- if (wants(K.branch.name)) {
155
- result.branch = { name: branch.name };
156
- }
157
- if (wantsAuth) {
158
- if (!authSnapshot) {
159
- throw new PlatformError(ErrorCode.NotFound, [
160
- `fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`,
161
- "Enable it via `apply(config, { projectId, branchId })` (or `npx neon …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false.",
162
- ].join(" "), {
163
- details: { projectId, branchId: branch.id },
164
- });
165
- }
166
- const auth = {};
167
- if (wants(K.auth.baseUrl))
168
- auth.baseUrl = authSnapshot.baseUrl ?? "";
169
- if (wants(K.auth.jwksUrl))
170
- auth.jwksUrl = authSnapshot.jwksUrl ?? "";
171
- result.auth = auth;
172
- }
173
- if (wantsDataApi) {
174
- if (!dataApiSnapshot) {
175
- throw new PlatformError(ErrorCode.NotFound, [
176
- `fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${databaseName}.`,
177
- "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false.",
178
- ].join(" "), {
179
- details: {
180
- projectId,
181
- branchId: branch.id,
182
- databaseName,
183
- },
184
- });
185
- }
186
- result.dataApi = { url: dataApiSnapshot.url };
187
- }
188
- // Object storage + AI Gateway (Preview). A single branch credential backs whichever of
189
- // these the policy enables; functions never force one but ride along on its scopes. None
190
- // of this runs when the policy enables neither, so the Postgres / Auth / Data API path
191
- // never touches the credentials/storage endpoints (and keeps working on production, where
192
- // they may not exist yet).
193
- const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
194
- const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
195
- const wantsStorage = storageEnabled &&
196
- (wants(K.storage.accessKeyId) ||
197
- wants(K.storage.secretAccessKey) ||
198
- wants(K.storage.endpoint) ||
199
- wants(K.storage.region));
200
- const wantsGateway = gatewayEnabled &&
201
- (wants(K.aiGateway.apiKey) || wants(K.aiGateway.baseUrl));
202
- // A credential is minted only for its *secrets*. The endpoint, region and gateway host
203
- // are plain branch metadata, so selecting only those touches no credential at all — which
204
- // is how a caller holding valid secrets refreshes the rest without issuing a new one.
205
- const wantsCredential = (storageEnabled &&
206
- (wants(K.storage.accessKeyId) ||
207
- wants(K.storage.secretAccessKey))) ||
208
- (gatewayEnabled && wants(K.aiGateway.apiKey));
209
- if (wantsStorage || wantsGateway) {
210
- // Read the branch's storage settings *before* minting: a policy that declares buckets
211
- // on a branch without storage has to fail without having spent a credential on a
212
- // resolve that cannot succeed.
213
- let storage = null;
214
- if (wantsStorage) {
215
- storage = await api.getProjectBranchStorage(projectId, branch.id);
216
- if (!storage) {
217
- throw new PlatformError(ErrorCode.NotFound, [
218
- `fetchEnv: branch policy declares object storage (preview.buckets) but storage is not enabled on branch ${branch.name} (${branch.id}).`,
219
- "Enable it via `apply(config, { projectId, branchId })` (or in the Neon Console) — then re-run fetchEnv. Or remove preview.buckets.",
220
- ].join(" "), { details: { projectId, branchId: branch.id } });
221
- }
222
- }
223
- const secrets = wantsCredential
224
- ? await mintBranchCredential({
225
- api,
226
- projectId,
227
- branchId: branch.id,
228
- branchName: branch.name,
229
- scopes: previewCredentialScopes(desired.preview),
230
- })
231
- : null;
232
- if (storage) {
233
- const storageEnv = {};
234
- if (secrets && wants(K.storage.accessKeyId)) {
235
- storageEnv.accessKeyId = secrets.accessKeyId;
236
- }
237
- if (secrets && wants(K.storage.secretAccessKey)) {
238
- storageEnv.secretAccessKey = secrets.secretAccessKey;
239
- }
240
- if (wants(K.storage.endpoint)) {
241
- storageEnv.endpoint = storage.s3Endpoint;
242
- }
243
- if (wants(K.storage.region))
244
- storageEnv.region = storage.region;
245
- result.storage = storageEnv;
246
- }
247
- if (wantsGateway) {
248
- const gateway = {};
249
- if (secrets && wants(K.aiGateway.apiKey)) {
250
- gateway.apiKey = secrets.apiToken;
251
- }
252
- if (wants(K.aiGateway.baseUrl)) {
253
- // Bare branch-scoped gateway host derived from the branch's connection URI —
254
- // not the control-plane API origin (which doesn't serve the gateway). Clients
255
- // append the dialect route (/v1, /openai/v1, /anthropic/v1) themselves.
256
- gateway.baseUrl = aiGatewayBaseUrl(branch.id, unpooled.uri);
257
- }
258
- result.aiGateway = gateway;
259
- }
260
- }
261
- return result;
91
+ * The {@link fetchEnv} body, with the key selection as a plain argument and no generic
92
+ * narrowing. Exists for callers that compute the selection at runtime — notably
93
+ * {@link fetchEnvReusingSecrets}, which decides which keys it still needs by checking the
94
+ * branch — since the public overload's `keys` is bound to a literal union those callers cannot
95
+ * produce without asserting.
96
+ *
97
+ * `keys === null` selects everything the policy enables.
98
+ */
99
+ async function fetchEnvKeys(config, options, keys) {
100
+ const api = options.api ?? createApiFromOptions(options);
101
+ const projectId = options.projectId;
102
+ const { branch, desired } = await resolveBranchPolicy(config, options, api);
103
+ const selection = keys ? new Set(keys) : null;
104
+ const wants = (key) => selection === null || selection.has(key);
105
+ const result = {};
106
+ const [roles, databases] = await Promise.all([api.listBranchRoles(projectId, branch.id), api.listBranchDatabases(projectId, branch.id)]);
107
+ const roleName = pickRoleName(roles, branch, options.roleName);
108
+ const databaseName = pickDatabaseName(databases, branch, options.databaseName);
109
+ const K = NEON_ENV_VAR_KEYS;
110
+ const wantsAuth = desired.authEnabled && (wants(K.auth.baseUrl) || wants(K.auth.jwksUrl));
111
+ const wantsDataApi = desired.dataApiEnabled && wants(K.dataApi.url);
112
+ const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
113
+ api.getConnectionUri(projectId, {
114
+ branchId: branch.id,
115
+ databaseName,
116
+ roleName,
117
+ pooled: true
118
+ }),
119
+ api.getConnectionUri(projectId, {
120
+ branchId: branch.id,
121
+ databaseName,
122
+ roleName,
123
+ pooled: false
124
+ }),
125
+ wantsAuth ? api.getNeonAuth(projectId, branch.id) : Promise.resolve(null),
126
+ wantsDataApi ? api.getNeonDataApi(projectId, branch.id, databaseName) : Promise.resolve(null)
127
+ ]);
128
+ const postgres = {};
129
+ if (wants(K.postgres.databaseUrl)) postgres.databaseUrl = pooled.uri;
130
+ if (wants(K.postgres.databaseUrlUnpooled)) postgres.databaseUrlUnpooled = unpooled.uri;
131
+ if (Object.keys(postgres).length > 0) result.postgres = postgres;
132
+ if (wants(K.branch.name)) result.branch = { name: branch.name };
133
+ if (wantsAuth) {
134
+ if (!authSnapshot) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or `npx neon …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false."].join(" "), { details: {
135
+ projectId,
136
+ branchId: branch.id
137
+ } });
138
+ const auth = {};
139
+ if (wants(K.auth.baseUrl)) auth.baseUrl = authSnapshot.baseUrl ?? "";
140
+ if (wants(K.auth.jwksUrl)) auth.jwksUrl = authSnapshot.jwksUrl ?? "";
141
+ result.auth = auth;
142
+ }
143
+ if (wantsDataApi) {
144
+ if (!dataApiSnapshot) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${databaseName}.`, "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false."].join(" "), { details: {
145
+ projectId,
146
+ branchId: branch.id,
147
+ databaseName
148
+ } });
149
+ result.dataApi = { url: dataApiSnapshot.url };
150
+ }
151
+ const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
152
+ const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
153
+ const wantsStorage = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey) || wants(K.storage.endpoint) || wants(K.storage.region));
154
+ const wantsGateway = gatewayEnabled && (wants(K.aiGateway.apiKey) || wants(K.aiGateway.baseUrl));
155
+ const wantsCredential = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey)) || gatewayEnabled && wants(K.aiGateway.apiKey);
156
+ if (wantsStorage || wantsGateway) {
157
+ let storage = null;
158
+ if (wantsStorage) {
159
+ storage = await api.getProjectBranchStorage(projectId, branch.id);
160
+ if (!storage) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy declares object storage (preview.buckets) but storage is not enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or in the Neon Console) — then re-run fetchEnv. Or remove preview.buckets."].join(" "), { details: {
161
+ projectId,
162
+ branchId: branch.id
163
+ } });
164
+ }
165
+ const secrets = wantsCredential ? await mintBranchCredential({
166
+ api,
167
+ projectId,
168
+ branchId: branch.id,
169
+ branchName: branch.name,
170
+ scopes: previewCredentialScopes(desired.preview)
171
+ }) : null;
172
+ if (storage) {
173
+ const storageEnv = {};
174
+ if (secrets && wants(K.storage.accessKeyId)) storageEnv.accessKeyId = secrets.accessKeyId;
175
+ if (secrets && wants(K.storage.secretAccessKey)) storageEnv.secretAccessKey = secrets.secretAccessKey;
176
+ if (wants(K.storage.endpoint)) storageEnv.endpoint = storage.s3Endpoint;
177
+ if (wants(K.storage.region)) storageEnv.region = storage.region;
178
+ result.storage = storageEnv;
179
+ }
180
+ if (wantsGateway) {
181
+ const gateway = {};
182
+ if (secrets && wants(K.aiGateway.apiKey)) gateway.apiKey = secrets.apiToken;
183
+ if (wants(K.aiGateway.baseUrl)) gateway.baseUrl = aiGatewayBaseUrl(branch.id, unpooled.uri);
184
+ result.aiGateway = gateway;
185
+ }
186
+ }
187
+ return result;
262
188
  }
263
189
  /**
264
- * Resolve the target branch and evaluate the policy against it — the first thing any
265
- * branch-scoped operation needs. Shared by {@link fetchEnv} and {@link fetchEnvReusingSecrets}
266
- * so the two agree on which branch they're talking about and what it has enabled.
267
- */
268
- export async function resolveBranchPolicy(config, options, api) {
269
- const projectId = options.projectId;
270
- const branches = await api.listBranches(projectId);
271
- if (branches.length === 0) {
272
- throw new PlatformError(ErrorCode.BranchNotFound, [
273
- `fetchEnv: project ${projectId} has no branches.`,
274
- "Deploy your neon.ts policy (or create a branch) first, or pick a different project id.",
275
- ].join(" "), { details: { projectId } });
276
- }
277
- const branchRef = options.branch ?? options.branchId;
278
- if (!branchRef) {
279
- throw new PlatformError(ErrorCode.BranchNotFound, [
280
- "fetchEnv: no branch provided.",
281
- "Pass `branch` with a branch name (e.g. `main`) or id (`br-…`).",
282
- ].join(" "), { details: { projectId } });
283
- }
284
- const branch = resolveBranch(branchRef, branches);
285
- const desired = resolveConfig(config, {
286
- name: branch.name,
287
- id: branch.id,
288
- exists: true,
289
- ...(branch.parentId ? { parentId: branch.parentId } : {}),
290
- isDefault: branch.isDefault,
291
- isProtected: branch.protected,
292
- ...(branch.expiresAt ? { expiresAt: branch.expiresAt } : {}),
293
- });
294
- return { branch, desired };
190
+ * Resolve the target branch and evaluate the policy against it — the first thing any
191
+ * branch-scoped operation needs. Shared by {@link fetchEnv} and {@link fetchEnvReusingSecrets}
192
+ * so the two agree on which branch they're talking about and what it has enabled.
193
+ */
194
+ async function resolveBranchPolicy(config, options, api) {
195
+ const projectId = options.projectId;
196
+ const branches = await api.listBranches(projectId);
197
+ if (branches.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: project ${projectId} has no branches.`, "Deploy your neon.ts policy (or create a branch) first, or pick a different project id."].join(" "), { details: { projectId } });
198
+ const branchRef = options.branch ?? options.branchId;
199
+ if (!branchRef) throw new PlatformError(ErrorCode.BranchNotFound, ["fetchEnv: no branch provided.", "Pass `branch` with a branch name (e.g. `main`) or id (`br-…`)."].join(" "), { details: { projectId } });
200
+ const branch = resolveBranch(branchRef, branches);
201
+ return {
202
+ branch,
203
+ desired: resolveConfig(config, {
204
+ name: branch.name,
205
+ id: branch.id,
206
+ exists: true,
207
+ ...branch.parentId ? { parentId: branch.parentId } : {},
208
+ isDefault: branch.isDefault,
209
+ isProtected: branch.protected,
210
+ ...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
211
+ })
212
+ };
295
213
  }
296
214
  /**
297
- * Scopes the branch credential should carry for a resolved branch policy. Only object storage
298
- * and the AI Gateway *require* a credential; functions never force one (they have no credential
299
- * of their own), but `functions:invoke` is added to the scope set when a credential is already
300
- * being minted for storage / the AI Gateway, so the one credential can invoke the branch's
301
- * functions too. Returns `[]` only when nothing credential-bearing is enabled.
302
- */
303
- export function previewCredentialScopes(preview) {
304
- if (!preview)
305
- return [];
306
- const storage = preview.buckets.length > 0;
307
- const aiGateway = preview.aiGatewayEnabled;
308
- if (!storage && !aiGateway)
309
- return [];
310
- return deriveCredentialScopes({
311
- storage,
312
- aiGateway,
313
- functions: preview.functions.length > 0,
314
- });
215
+ * Scopes the branch credential should carry for a resolved branch policy. Only object storage
216
+ * and the AI Gateway *require* a credential; functions never force one (they have no credential
217
+ * of their own), but `functions:invoke` is added to the scope set when a credential is already
218
+ * being minted for storage / the AI Gateway, so the one credential can invoke the branch's
219
+ * functions too. Returns `[]` only when nothing credential-bearing is enabled.
220
+ */
221
+ function previewCredentialScopes(preview) {
222
+ if (!preview) return [];
223
+ const storage = preview.buckets.length > 0;
224
+ const aiGateway = preview.aiGatewayEnabled;
225
+ if (!storage && !aiGateway) return [];
226
+ return deriveCredentialScopes({
227
+ storage,
228
+ aiGateway,
229
+ functions: preview.functions.length > 0
230
+ });
315
231
  }
316
232
  /** The `name` this tool stamps on every credential it mints, so it can recognize its own. */
317
- export function credentialName(branchName) {
318
- return `neon-env ${branchName}`;
233
+ function credentialName(branchName) {
234
+ return `neon-env ${branchName}`;
319
235
  }
320
236
  /** The env-var keys a branch credential's secrets surface under, in emit order. */
321
- export function credentialEnvKeys(flags) {
322
- return [
323
- ...(flags.storage
324
- ? [
325
- NEON_ENV_VAR_KEYS.storage.accessKeyId,
326
- NEON_ENV_VAR_KEYS.storage.secretAccessKey,
327
- ]
328
- : []),
329
- ...(flags.aiGateway ? [NEON_ENV_VAR_KEYS.aiGateway.apiKey] : []),
330
- ];
237
+ function credentialEnvKeys(flags) {
238
+ return [...flags.storage ? [NEON_ENV_VAR_KEYS.storage.accessKeyId, NEON_ENV_VAR_KEYS.storage.secretAccessKey] : [], ...flags.aiGateway ? [NEON_ENV_VAR_KEYS.aiGateway.apiKey] : []];
331
239
  }
332
240
  /**
333
- * Every OS-level env var a resolved branch policy produces, in emit order. Lets a caller
334
- * subtract the ones it already holds and pass the rest as {@link fetchEnv}'s `keys`, without
335
- * re-deriving which vars a policy implies.
336
- */
337
- export function policyEnvKeys(desired) {
338
- const K = NEON_ENV_VAR_KEYS;
339
- return [
340
- K.postgres.databaseUrl,
341
- K.postgres.databaseUrlUnpooled,
342
- K.branch.name,
343
- ...(desired.authEnabled ? [K.auth.baseUrl, K.auth.jwksUrl] : []),
344
- ...(desired.dataApiEnabled ? [K.dataApi.url] : []),
345
- ...((desired.preview?.buckets.length ?? 0) > 0
346
- ? [
347
- K.storage.accessKeyId,
348
- K.storage.secretAccessKey,
349
- K.storage.endpoint,
350
- K.storage.region,
351
- ]
352
- : []),
353
- ...(desired.preview?.aiGatewayEnabled
354
- ? [K.aiGateway.apiKey, K.aiGateway.baseUrl]
355
- : []),
356
- ];
241
+ * Every OS-level env var a resolved branch policy produces, in emit order. Lets a caller
242
+ * subtract the ones it already holds and pass the rest as {@link fetchEnv}'s `keys`, without
243
+ * re-deriving which vars a policy implies.
244
+ */
245
+ function policyEnvKeys(desired) {
246
+ const K = NEON_ENV_VAR_KEYS;
247
+ return [
248
+ K.postgres.databaseUrl,
249
+ K.postgres.databaseUrlUnpooled,
250
+ K.branch.name,
251
+ ...desired.authEnabled ? [K.auth.baseUrl, K.auth.jwksUrl] : [],
252
+ ...desired.dataApiEnabled ? [K.dataApi.url] : [],
253
+ ...(desired.preview?.buckets.length ?? 0) > 0 ? [
254
+ K.storage.accessKeyId,
255
+ K.storage.secretAccessKey,
256
+ K.storage.endpoint,
257
+ K.storage.region
258
+ ] : [],
259
+ ...desired.preview?.aiGatewayEnabled ? [K.aiGateway.apiKey, K.aiGateway.baseUrl] : []
260
+ ];
357
261
  }
358
262
  /**
359
- * Mint the branch credential backing object storage / the AI Gateway.
360
- *
361
- * `api_token` and `s3_secret_access_key` come back **exactly once** — they are not stored
362
- * server-side and the list endpoint returns metadata only — so the caller's copy is the only
363
- * copy. That is why {@link fetchEnv} mints rather than fetches: there is nothing to fetch. A
364
- * caller that already holds a valid copy should leave the secret keys out of `keys` (see
365
- * {@link fetchEnvReusingSecrets}) instead of minting one it will discard.
366
- */
263
+ * Mint the branch credential backing object storage / the AI Gateway.
264
+ *
265
+ * `api_token` and `s3_secret_access_key` come back **exactly once** — they are not stored
266
+ * server-side and the list endpoint returns metadata only — so the caller's copy is the only
267
+ * copy. That is why {@link fetchEnv} mints rather than fetches: there is nothing to fetch. A
268
+ * caller that already holds a valid copy should leave the secret keys out of `keys` (see
269
+ * {@link fetchEnvReusingSecrets}) instead of minting one it will discard.
270
+ */
367
271
  async function mintBranchCredential(args) {
368
- const minted = await args.api.createCredential(args.projectId, args.branchId, {
369
- scopes: args.scopes,
370
- principalType: "user",
371
- name: credentialName(args.branchName),
372
- });
373
- return {
374
- // The storage gateway authenticates against the full token id (e.g.
375
- // `nak_live_…`), not the short token id — using the short id yields
376
- // `InvalidAccessKeyId` on every S3 request.
377
- accessKeyId: minted.tokenId,
378
- secretAccessKey: minted.s3SecretAccessKey,
379
- apiToken: minted.apiToken,
380
- };
272
+ const minted = await args.api.createCredential(args.projectId, args.branchId, {
273
+ scopes: args.scopes,
274
+ principalType: "user",
275
+ name: credentialName(args.branchName)
276
+ });
277
+ return {
278
+ accessKeyId: minted.tokenId,
279
+ secretAccessKey: minted.s3SecretAccessKey,
280
+ apiToken: minted.apiToken
281
+ };
381
282
  }
382
283
  /**
383
- * The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
384
- * control-plane API origin. Derive the suffix from the branch's own Postgres connection host
385
- * by dropping only the endpoint label (the first segment) and keeping everything after it,
386
- * including any infra cell prefix (`c-N.`): a connection host of
387
- * `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
388
- * `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
389
- * the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
390
- */
284
+ * The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
285
+ * control-plane API origin. Derive the suffix from the branch's own Postgres connection host
286
+ * by dropping only the endpoint label (the first segment) and keeping everything after it,
287
+ * including any infra cell prefix (`c-N.`): a connection host of
288
+ * `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
289
+ * `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
290
+ * the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
291
+ */
391
292
  function aiGatewayHost(branchId, connectionUri) {
392
- let connectionHost = "";
393
- try {
394
- connectionHost = new URL(connectionUri).hostname;
395
- }
396
- catch {
397
- connectionHost = "";
398
- }
399
- // Drop the endpoint label (first segment, e.g. `ep-x` / `ep-x-pooler`), keeping the rest
400
- // of the host verbatim — including any infra cell prefix (`c-N.`) the gateway routes on:
401
- // `[c-N.]<region>.<cloud>.neon.<tld>`.
402
- const suffix = connectionHost.split(".").slice(1).join(".");
403
- return `${branchId}-api.ai.${suffix}`;
293
+ let connectionHost = "";
294
+ try {
295
+ connectionHost = new URL(connectionUri).hostname;
296
+ } catch {
297
+ connectionHost = "";
298
+ }
299
+ return `${branchId}-api.ai.${connectionHost.split(".").slice(1).join(".")}`;
404
300
  }
405
301
  /** The AI Gateway's bare base URL (`NEON_AI_GATEWAY_BASE_URL`) on the branch gateway host. */
406
302
  function aiGatewayBaseUrl(branchId, connectionUri) {
407
- return `https://${aiGatewayHost(branchId, connectionUri)}`;
303
+ return `https://${aiGatewayHost(branchId, connectionUri)}`;
408
304
  }
409
- export function createApiFromOptions(options) {
410
- return createNeonApiFromOptions("fetchEnv", {
411
- ...(options.apiKey ? { apiKey: options.apiKey } : {}),
412
- ...(options.apiHost ? { apiHost: options.apiHost } : {}),
413
- });
305
+ function createApiFromOptions(options) {
306
+ return createNeonApiFromOptions("fetchEnv", {
307
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
308
+ ...options.apiHost ? { apiHost: options.apiHost } : {}
309
+ });
414
310
  }
415
311
  /**
416
- * Resolve a branch ref — a name or an id — to a concrete branch. Matches by id first
417
- * (exact `br-…`), then by name; both are unique within a project, so the lookup is
418
- * unambiguous. This lets `.neon` files written by `neonctl` (which pin the branch *name*)
419
- * and explicit `br-…` ids both work.
420
- */
312
+ * Resolve a branch ref — a name or an id — to a concrete branch. Matches by id first
313
+ * (exact `br-…`), then by name; both are unique within a project, so the lookup is
314
+ * unambiguous. This lets `.neon` files written by `neonctl` (which pin the branch *name*)
315
+ * and explicit `br-…` ids both work.
316
+ */
421
317
  function resolveBranch(branch, branches) {
422
- const match = branches.find((b) => b.id === branch) ??
423
- branches.find((b) => b.name === branch);
424
- if (match)
425
- return match;
426
- throw new PlatformError(ErrorCode.BranchNotFound, [
427
- `fetchEnv: branch ${JSON.stringify(branch)} not found on project (matched by id or name).`,
428
- `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`,
429
- ].join(" "), {
430
- details: {
431
- branch,
432
- available: branches.map((b) => `${b.name} (${b.id})`),
433
- },
434
- });
318
+ const match = branches.find((b) => b.id === branch) ?? branches.find((b) => b.name === branch);
319
+ if (match) return match;
320
+ throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${JSON.stringify(branch)} not found on project (matched by id or name).`, `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`].join(" "), { details: {
321
+ branch,
322
+ available: branches.map((b) => `${b.name} (${b.id})`)
323
+ } });
435
324
  }
436
325
  function pickRoleName(roles, branch, requested) {
437
- if (requested) {
438
- if (!roles.some((r) => r.name === requested)) {
439
- throw new PlatformError(ErrorCode.BranchNotFound, [
440
- `fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`,
441
- `Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`,
442
- ].join(" "), {
443
- details: {
444
- branchId: branch.id,
445
- roleName: requested,
446
- availableRoles: roles.map((r) => r.name),
447
- },
448
- });
449
- }
450
- return requested;
451
- }
452
- if (roles.length === 0) {
453
- throw new PlatformError(ErrorCode.BranchNotFound, [
454
- `fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`,
455
- "Create one via the Neon console or pass `roleName` explicitly.",
456
- ].join(" "), { details: { branchId: branch.id } });
457
- }
458
- if (roles.length === 1)
459
- return roles[0].name;
460
- // Multiple roles. Enabling Neon Auth / the Data API provisions the PostgREST roles
461
- // (authenticator/anonymous/authenticated) alongside the project owner, so a normal
462
- // branch ends up with >1 role even though only the owner backs a `DATABASE_URL`.
463
- // Default to Neon's owner role; if the project was created with a custom owner name,
464
- // fall back to the single role left after dropping the managed auth roles. Only a
465
- // genuinely ambiguous set (more than one app role) still asks the caller to choose.
466
- const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
467
- if (owner)
468
- return owner.name;
469
- const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
470
- if (appRoles.length === 1)
471
- return appRoles[0].name;
472
- throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [
473
- `fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles and none is "${NEON_DEFAULT_OWNER_ROLE}"; cannot auto-pick.`,
474
- `Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`,
475
- ].join(" "), {
476
- details: {
477
- branchId: branch.id,
478
- availableRoles: roles.map((r) => r.name),
479
- },
480
- });
326
+ if (requested) {
327
+ if (!roles.some((r) => r.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`].join(" "), { details: {
328
+ branchId: branch.id,
329
+ roleName: requested,
330
+ availableRoles: roles.map((r) => r.name)
331
+ } });
332
+ return requested;
333
+ }
334
+ if (roles.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`, "Create one via the Neon console or pass `roleName` explicitly."].join(" "), { details: { branchId: branch.id } });
335
+ if (roles.length === 1) return roles[0].name;
336
+ const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
337
+ if (owner) return owner.name;
338
+ const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
339
+ if (appRoles.length === 1) return appRoles[0].name;
340
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles and none is "${NEON_DEFAULT_OWNER_ROLE}"; cannot auto-pick.`, `Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`].join(" "), { details: {
341
+ branchId: branch.id,
342
+ availableRoles: roles.map((r) => r.name)
343
+ } });
481
344
  }
482
345
  function pickDatabaseName(databases, branch, requested) {
483
- if (requested) {
484
- if (!databases.some((d) => d.name === requested)) {
485
- throw new PlatformError(ErrorCode.BranchNotFound, [
486
- `fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`,
487
- `Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`,
488
- ].join(" "), {
489
- details: {
490
- branchId: branch.id,
491
- databaseName: requested,
492
- availableDatabases: databases.map((d) => d.name),
493
- },
494
- });
495
- }
496
- return requested;
497
- }
498
- if (databases.length === 0) {
499
- throw new PlatformError(ErrorCode.BranchNotFound, [
500
- `fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`,
501
- "Create one via the Neon console or pass `databaseName` explicitly.",
502
- ].join(" "), { details: { branchId: branch.id } });
503
- }
504
- // Prefer Neon's default `neondb`. On the common "added a second database" branch this
505
- // auto-picks it, so a lone or `neondb`-including branch resolves without asking.
506
- const neondb = databases.find((d) => d.name === NEON_DEFAULT_DATABASE);
507
- if (neondb)
508
- return neondb.name;
509
- if (databases.length === 1)
510
- return databases[0].name;
511
- // Several databases and no `neondb` to fall back on. Auto-picking any of them would be
512
- // perceived as random and is bad DX, so fail loudly and let the caller disambiguate.
513
- throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [
514
- `fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases and none is named "${NEON_DEFAULT_DATABASE}"; cannot auto-pick.`,
515
- `Rename one to "${NEON_DEFAULT_DATABASE}" or keep a single database on the branch (or, when calling fetchEnv directly, pass \`databaseName\`). Available: ${databases.map((d) => d.name).join(", ")}.`,
516
- ].join(" "), {
517
- details: {
518
- branchId: branch.id,
519
- availableDatabases: databases.map((d) => d.name),
520
- },
521
- });
346
+ if (requested) {
347
+ if (!databases.some((d) => d.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`].join(" "), { details: {
348
+ branchId: branch.id,
349
+ databaseName: requested,
350
+ availableDatabases: databases.map((d) => d.name)
351
+ } });
352
+ return requested;
353
+ }
354
+ if (databases.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`, "Create one via the Neon console or pass `databaseName` explicitly."].join(" "), { details: { branchId: branch.id } });
355
+ const neondb = databases.find((d) => d.name === NEON_DEFAULT_DATABASE);
356
+ if (neondb) return neondb.name;
357
+ if (databases.length === 1) return databases[0].name;
358
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases and none is named "${NEON_DEFAULT_DATABASE}"; cannot auto-pick.`, `Rename one to "${NEON_DEFAULT_DATABASE}" or keep a single database on the branch (or, when calling fetchEnv directly, pass \`databaseName\`). Available: ${databases.map((d) => d.name).join(", ")}.`].join(" "), { details: {
359
+ branchId: branch.id,
360
+ availableDatabases: databases.map((d) => d.name)
361
+ } });
522
362
  }
523
- // ───────────────────────── env-var mapping helpers ─────────────────────────
524
363
  /**
525
- * Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
526
- * for cross-process transport. Named after the web-platform `.entries()` convention
527
- * (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
528
- * iterator of tuples since that's the shape env injection needs (wrap with
529
- * `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
530
- * to inject the vars into a subprocess's `process.env`.
531
- *
532
- * Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
533
- * conditional namespaces are present.
534
- */
535
- export function toEntries(env) {
536
- const out = {};
537
- const put = (key, value) => {
538
- if (value !== undefined)
539
- out[key] = value;
540
- };
541
- const K = NEON_ENV_VAR_KEYS;
542
- put(K.postgres.databaseUrl, env.postgres?.databaseUrl);
543
- put(K.postgres.databaseUrlUnpooled, env.postgres?.databaseUrlUnpooled);
544
- put(K.branch.name, env.branch?.name);
545
- put(K.auth.baseUrl, env.auth?.baseUrl);
546
- put(K.auth.jwksUrl, env.auth?.jwksUrl);
547
- put(K.dataApi.url, env.dataApi?.url);
548
- put(K.storage.accessKeyId, env.storage?.accessKeyId);
549
- put(K.storage.secretAccessKey, env.storage?.secretAccessKey);
550
- put(K.storage.endpoint, env.storage?.endpoint);
551
- put(K.storage.region, env.storage?.region);
552
- // Neon-branded gateway vars only: the bearer and the bare branch gateway host
553
- // (scheme://host, no path) — the @neon/ai-sdk-provider appends the dialect route
554
- // (/v1, /openai/v1, /anthropic/v1) itself (https://github.com/vercel/ai/pull/15997).
555
- put(K.aiGateway.apiKey, env.aiGateway?.apiKey);
556
- put(K.aiGateway.baseUrl, env.aiGateway?.baseUrl);
557
- return out;
364
+ * Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
365
+ * for cross-process transport. Named after the web-platform `.entries()` convention
366
+ * (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
367
+ * iterator of tuples since that's the shape env injection needs (wrap with
368
+ * `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
369
+ * to inject the vars into a subprocess's `process.env`.
370
+ *
371
+ * Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
372
+ * conditional namespaces are present.
373
+ */
374
+ function toEntries(env) {
375
+ const out = {};
376
+ const put = (key, value) => {
377
+ if (value !== void 0) out[key] = value;
378
+ };
379
+ const K = NEON_ENV_VAR_KEYS;
380
+ put(K.postgres.databaseUrl, env.postgres?.databaseUrl);
381
+ put(K.postgres.databaseUrlUnpooled, env.postgres?.databaseUrlUnpooled);
382
+ put(K.branch.name, env.branch?.name);
383
+ put(K.auth.baseUrl, env.auth?.baseUrl);
384
+ put(K.auth.jwksUrl, env.auth?.jwksUrl);
385
+ put(K.dataApi.url, env.dataApi?.url);
386
+ put(K.storage.accessKeyId, env.storage?.accessKeyId);
387
+ put(K.storage.secretAccessKey, env.storage?.secretAccessKey);
388
+ put(K.storage.endpoint, env.storage?.endpoint);
389
+ put(K.storage.region, env.storage?.region);
390
+ put(K.aiGateway.apiKey, env.aiGateway?.apiKey);
391
+ put(K.aiGateway.baseUrl, env.aiGateway?.baseUrl);
392
+ return out;
558
393
  }
394
+ //#endregion
395
+ export { NEON_ENV_VAR_KEYS, createApiFromOptions, credentialEnvKeys, credentialName, fetchEnv, fetchEnvKeys, policyEnvKeys, previewCredentialScopes, resolveBranchPolicy, toEntries };