neon 4.13.1 → 4.14.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 (40) hide show
  1. package/README.md +51 -9
  2. package/dist/_chunks/{credential_io-YeAxg9Xn.js → credential_io-D8AR-jvB.js} +4 -4
  3. package/dist/_chunks/{env-NbA61JR3.js → env-CZmstYrY.js} +71 -59
  4. package/dist/_chunks/{env_services-Tz9G4JeT.js → env_services-CAWrZTWa.js} +138 -41
  5. package/dist/_shared/auth_selection.js +83 -0
  6. package/dist/_shared/credentials.js +188 -0
  7. package/dist/_shared/paths.js +151 -0
  8. package/dist/_shared/profiles.js +226 -0
  9. package/dist/_shared/secure_file.js +41 -0
  10. package/dist/analytics.js +2 -2
  11. package/dist/auth.js +101 -11
  12. package/dist/auth_context.js +2 -11
  13. package/dist/claimable/api.js +2 -1
  14. package/dist/commands/auth.js +155 -63
  15. package/dist/commands/checkout.js +28 -9
  16. package/dist/commands/claim.js +11 -7
  17. package/dist/commands/config.js +9 -9
  18. package/dist/commands/deploy.js +2 -1
  19. package/dist/commands/deploy_help.js +10 -0
  20. package/dist/commands/dev.js +147 -36
  21. package/dist/commands/env.js +33 -44
  22. package/dist/commands/functions.js +63 -2
  23. package/dist/commands/profile.js +2 -2
  24. package/dist/config_services.js +36 -1
  25. package/dist/credential_io.js +1 -1
  26. package/dist/custom_domains_api.js +37 -0
  27. package/dist/dev/env.js +1 -1
  28. package/dist/dev/functions.js +28 -11
  29. package/dist/dev/websocket.js +1 -1
  30. package/dist/env_services.js +2 -2
  31. package/dist/index.js +73 -20
  32. package/dist/init/build_config.js +4 -0
  33. package/dist/neon_services.js +2 -2
  34. package/dist/psql/wire/connection.js +1 -1
  35. package/dist/refresh_lock.js +44 -0
  36. package/dist/retire_credential.js +1 -1
  37. package/dist/test_utils/neon_api_server.js +60 -0
  38. package/dist/test_utils/rotating_oauth_server.js +94 -0
  39. package/dist/utils/config_diff.js +2 -13
  40. package/package.json +8 -6
package/README.md CHANGED
@@ -78,9 +78,36 @@ neon claim create --service auth --service data-api
78
78
  ```
79
79
 
80
80
  When the current directory has a `neon.ts`, `claim create` also requests every service
81
- declared there. Explicit `--service` values are added to that set. Object Storage, Functions,
82
- and the AI Gateway are sent to the service so demand is recorded, but are reported as
83
- unavailable until the project is claimed; the CLI does not silently remove them.
81
+ declared there. Explicit `--service` values are added to that set. If that policy enables
82
+ the Data API, identity includes a snake_case `data_api` object. A neon.ts with
83
+ `dataApi: { authProvider: "external", jwksUrl: "https://idp.example.com/jwks.json" }`
84
+ sends:
85
+
86
+ ```json
87
+ {
88
+ "type": "anonymous",
89
+ "capabilities": ["postgres", "data_api"],
90
+ "source": "neon_cli",
91
+ "data_api": {
92
+ "auth_provider": "external",
93
+ "jwks_url": "https://idp.example.com/jwks.json"
94
+ }
95
+ }
96
+ ```
97
+
98
+ `neon claim create --service data-api` with no neon.ts Data API block omits `data_api`:
99
+
100
+ ```json
101
+ {
102
+ "type": "anonymous",
103
+ "capabilities": ["postgres", "data_api"],
104
+ "source": "neon_cli"
105
+ }
106
+ ```
107
+
108
+ Object Storage, Functions, and the AI Gateway are sent to the service so demand is
109
+ recorded, but are reported as unavailable until the project is claimed; the CLI does
110
+ not silently remove them.
84
111
 
85
112
  The command writes:
86
113
 
@@ -467,8 +494,8 @@ The human-readable summary line goes to stderr and the diff body to stdout, so `
467
494
  **What gets pulled**, in precedence order:
468
495
 
469
496
  1. **`--service` and/or `--env`**, when you pass either — their union is the complete selection, ignoring `neon.ts` and unselected branch variables. `--service` adds a service's complete variable bundle; `--env` adds only the individual variables you name.
470
- 2. **`neon.ts`**, when the working directory has one — the policy is the source of truth, same as `neon dev` and `neon deploy`.
471
- 3. **Everything the branch has** otherwise — Postgres, Neon Auth, the Data API, and object storage read back from the branch, plus the AI Gateway. The gateway has no branch-level state to read back, so a bare `env pull` asks for it rather than detecting it and may mint a branch credential. To leave it out, name only what you do want with `--service` and/or `--env`.
497
+ 2. **`neon.ts`**, when the working directory has one — the policy is the source of truth, same as `neon dev` and `neon deploy`. Declared function URLs are derived from the branch connection host; the function does not have to be deployed.
498
+ 3. **Everything the branch has** otherwise — Postgres, Neon Auth, the Data API, object storage, and function invocation URLs read back from the branch, plus the AI Gateway. The gateway has no branch-level state to read back, so a bare `env pull` asks for it rather than detecting it and may mint a branch credential. To leave it out, name only what you do want with `--service` and/or `--env`.
472
499
 
473
500
  If the gateway can't be resolved, it is dropped with a warning and the rest of the pull still lands. Gateway variables already in your file for *this* branch are left alone — a pull that couldn't reach the gateway is no evidence the branch has stopped having one — while ones left over from a different branch are pruned like any other stale value.
474
501
 
@@ -492,6 +519,10 @@ neon env pull -e DATABASE_URL,NEON_AUTH_BASE_URL
492
519
 
493
520
  # The selectors compose as a union: all Auth vars plus DATABASE_URL
494
521
  neon env pull -s auth -e DATABASE_URL
522
+
523
+ # Function invocation URLs
524
+ neon env pull -s functions
525
+ neon env pull -e NEON_FUNCTION_HELLO_BASE_URL
495
526
  ```
496
527
 
497
528
  Every services flag in the CLI takes those three spellings, the same value syntax, and the same service names — see [`config init --services`](#getting-a-neonts-config-init).
@@ -501,14 +532,15 @@ Every services flag in the CLI takes those three spellings, the same value synta
501
532
  | `postgres` | `DATABASE_URL`, `DATABASE_URL_UNPOOLED` |
502
533
  | `auth` | `NEON_AUTH_BASE_URL`, `NEON_AUTH_JWKS_URL` |
503
534
  | `data-api` | `NEON_DATA_API_URL` |
535
+ | `functions` | `NEON_FUNCTION_<SLUG>_BASE_URL` for each deployed function |
504
536
  | `object-storage` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT_URL_S3`, `AWS_REGION` |
505
537
  | `ai-gateway` | `NEON_AI_GATEWAY_TOKEN`, `NEON_AI_GATEWAY_BASE_URL` |
506
538
 
507
- `-e, --env` accepts any variable in the table plus `NEON_BRANCH`. It is case-sensitive and rejects unknown names rather than silently widening the pull. `NEON_BRANCH` is written by unscoped and service-scoped pulls because it is branch identity, not a service; an env-only pull writes it only when you select it.
539
+ `-e, --env` accepts any variable in the table plus `NEON_BRANCH`, or `NEON_FUNCTION_<SLUG>_BASE_URL` where `<SLUG>` is the function slug uppercased. It is case-sensitive and rejects unknown names rather than silently widening the pull. `NEON_BRANCH` is written by unscoped and service-scoped pulls because it is branch identity, not a service; an env-only pull writes it only when you select it.
508
540
 
509
541
  `--env` never narrows `--service`: `neon env pull -s postgres -e DATABASE_URL` still pulls the complete Postgres bundle (`DATABASE_URL`, `DATABASE_URL_UNPOOLED`, and `NEON_BRANCH`). The two selectors always form a union.
510
542
 
511
- **A scoped pull is scoped in both directions.** An unscoped `env pull` owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale `NEON_AUTH_*` lines. `--service` narrows that to the services you named, while `--env` narrows it to the exact keys you named, so `env pull -e DATABASE_URL` never touches `DATABASE_URL_UNPOOLED`. (`AWS_*` is never pruned by any pull: those names collide with credentials you may set yourself, so `env pull` only ever writes them.)
543
+ **A scoped pull is scoped in both directions.** An unscoped `env pull` owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale `NEON_AUTH_*` lines, and a branch without those functions prunes stale `NEON_FUNCTION_*_BASE_URL` lines. `--service` narrows that to the services you named, while `--env` narrows it to the exact keys you named, so `env pull -e DATABASE_URL` never touches `DATABASE_URL_UNPOOLED`. (`AWS_*` is never pruned by any pull: those names collide with credentials you may set yourself, so `env pull` only ever writes them.)
512
544
 
513
545
  **A scoped pull also never revokes a credential.** Where an unscoped pull revokes the credential it replaces, a scoped one leaves the old one live — it can't tell which other variables still use it. It says so when it happens; revoke it in the Neon Console if nothing does.
514
546
 
@@ -526,7 +558,7 @@ Console), or drop auth from --service.
526
558
 
527
559
  If you'd rather not keep env vars on disk, inject them at runtime instead with `neon-env run -- <your dev command>` (from `@neon/env`) or `neon dev`, and pass `--no-env-pull` to `link` / `checkout`.
528
560
 
529
- **`neon dev` resolves the same set, by the same rules** — including the AI Gateway on a branch with no `neon.ts`. A function running locally gets what the deployed runtime would inject into it, which is the whole point of `dev`; a handler that reads `NEON_AI_GATEWAY_BASE_URL` should not work in production and fail on your machine. `dev` writes nothing, but it does *read* your `.env` / `.env.local` to reuse the branch credential behind the AI Gateway and object storage. Without a file to read from it issues one on every start and leaves the last one live — it has nowhere to keep it, and so cannot name it to revoke it. It says so when it happens; run `env pull` (or just `link` / `checkout`) once and restarts reuse the credential instead.
561
+ **`neon dev` resolves the same set, by the same rules** — including the AI Gateway on a branch with no `neon.ts`. A function running locally gets the same Postgres / Auth / Data API / storage / AI Gateway vars a deploy would inject. Each `NEON_FUNCTION_<SLUG>_BASE_URL` this process is serving is rewritten to `http://localhost:<port>`: every neon.ts function, or the matching slug when `--source` names one. `neon env pull` and `neon-env run` write the production URL (`https://<branchId>-<slug>.compute.…`). `dev` writes nothing, but it does *read* your `.env` / `.env.local` to reuse the branch credential behind the AI Gateway and object storage. Without a file to read from it issues one on every start and leaves the last one live — it has nowhere to keep it, and so cannot name it to revoke it. It says so when it happens; run `env pull` (or just `link` / `checkout`) once and restarts reuse the credential instead.
530
562
 
531
563
  **Where `.neon` lives**: `link` writes `.neon` into the **current working directory** by default. If an existing `.neon` is found in any parent directory, that file is reused — so commands run from a sub-directory of a linked project still pick up the project's context. To pin the location explicitly, pass `--context-file <path>`.
532
564
 
@@ -677,6 +709,16 @@ neon deploy --branch my-feature --update-existing
677
709
 
678
710
  Function deploys declared under `preview.functions` are bundled with esbuild by default. A directory `source` is discovered as `index.ts`, then `index.js`, then `index.mjs`. Set `bundler: "none"` to ship a prebuilt directory as-is. `neon function deploy --no-bundle` is the same switch without a `neon.ts`.
679
711
 
712
+ Point a domain you already own at a function with `neon function domains` (alias `domain`):
713
+
714
+ ```bash
715
+ neon function domains register docs.example.com --slug api
716
+ neon function domains list
717
+ neon function domains delete docs.example.com
718
+ ```
719
+
720
+ Register prints the record, then an INFO line `CNAME <domain> to <cname_target>`. The domain goes live after that CNAME resolves and a certificate is issued on the first request.
721
+
680
722
  When a package cannot be bundled — a native addon with no esbuild loader, or an optional peer dependency a library references on an untaken code path — list it in that function's `externalPackages` and the bundler leaves the import alone. `neon dev` honours the same list. It does not make the package resolvable in the deployed archive (there is no `node_modules` next to the bundle), so it only unblocks an import that is never evaluated — a dependency the handler actually calls has to be bundled, and a natively-backed one cannot be. See [`@neon/config`](../config/README.md#unbundleable-dependencies-externalpackages).
681
723
 
682
724
  ## Scaffold a project (`bootstrap`)
@@ -1224,7 +1266,7 @@ Id Name Project Created At Last Used At Last
1224
1266
  | [me](https://neon.com/docs/reference/cli-me) | | Show current user |
1225
1267
  | [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches |
1226
1268
  | [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
1227
- | function | `deploy`, `list`, `get`, `delete` | Manage Neon Functions |
1269
+ | function | `deploy`, `list`, `get`, `delete`, `domains list`, `domains register`, `domains delete` | Manage Neon Functions |
1228
1270
  | [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
1229
1271
  | [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
1230
1272
  | logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
@@ -236,7 +236,7 @@ const createCredentialStore = (dir, options = {}) => {
236
236
  const assertKeyringWritable = (profile) => {
237
237
  if (keyring === null) throw new KeyringUnavailableError(profile, "write");
238
238
  };
239
- const setKeyringOrRollback = (profile, credentials) => {
239
+ const setKeyringOrRollback = (profile, credentials, restorePrevious) => {
240
240
  assertKeyringWritable(profile);
241
241
  const kr = keyring;
242
242
  if (kr === null) throw new KeyringUnavailableError(profile, "write");
@@ -256,7 +256,7 @@ const createCredentialStore = (dir, options = {}) => {
256
256
  try {
257
257
  if (kr.get("com.neon.neon-cli", account) === null) throw new Error(`Wrote credentials to the OS keyring for ${label} but could not read them back.`);
258
258
  } catch (err) {
259
- if (previous !== null) {
259
+ if (restorePrevious && previous !== null) {
260
260
  try {
261
261
  kr.set(KEYRING_SERVICE, account, previous);
262
262
  } catch {
@@ -372,9 +372,9 @@ const createCredentialStore = (dir, options = {}) => {
372
372
  profile: at.profile
373
373
  };
374
374
  };
375
- const write = (at, credentials) => {
375
+ const write = (at, credentials, options) => {
376
376
  if (at.storage === "keyring") {
377
- setKeyringOrRollback(at.profile, credentials);
377
+ setKeyringOrRollback(at.profile, credentials, options?.restorePrevious !== false);
378
378
  return {
379
379
  credentials,
380
380
  backend: CRED_STORAGE_KEYRING,
@@ -1,9 +1,9 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
- import { _ as policyEnvKeys, a as envKeysForSelection, b as toEntries, d as servicesForEnvKeys, f as NEON_ENV_VAR_KEYS, g as fetchEnvKeys, h as credentialName, m as credentialEnvKeys, p as createApiFromOptions, r as ENV_PULL_SERVICES, u as serviceForEnvKey, v as previewCredentialScopes, y as resolveBranchPolicy } from "./env_services-Tz9G4JeT.js";
3
+ import { S as toEntries, _ as functionBaseUrlKey, a as envKeysForSelection, b as previewCredentialScopes, d as servicesForEnvKeys, f as NEON_ENV_VAR_KEYS, g as fetchEnvKeysState, h as credentialName, i as ENV_PULL_SERVICES, m as credentialEnvKeys, p as createApiFromOptions, u as serviceForEnvKey, v as isFunctionBaseUrlKey, x as resolveBranchPolicy, y as policyEnvKeys } from "./env_services-CAWrZTWa.js";
4
4
  import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
5
5
  import { credentialScopesSatisfied } from "@neon/config/v1";
6
- import { ErrorCode as ErrorCode$1, createNeonApiFromOptions as createNeonApiFromOptions$1, isPlatformError, loadConfigFromFile } from "@neon/config";
6
+ import { ErrorCode as ErrorCode$1, createNeonApiFromOptions as createNeonApiFromOptions$1, isPlatformError as isPlatformError$1, loadConfigFromFile } from "@neon/config";
7
7
  import { plan, pullConfig } from "@neon/config-runtime";
8
8
  //#region ../../internals/env-core/dist/reuse-secrets.js
9
9
  /**
@@ -45,7 +45,7 @@ async function fetchEnvReusingSecrets(config, options) {
45
45
  const { branch, desired } = await resolveBranchPolicy(config, options, api);
46
46
  const allPolicyKeys = policyEnvKeys(desired);
47
47
  const requested = requestedKeys ? new Set(requestedKeys) : null;
48
- const selectedPolicyKeys = requested === null ? allPolicyKeys : allPolicyKeys.filter((key) => requested.has(key));
48
+ const selectedPolicyKeys = requested === null ? allPolicyKeys : [...allPolicyKeys.filter((key) => requested.has(key)), ...[...requested].filter(isFunctionBaseUrlKey).sort()];
49
49
  const selected = new Set(selectedPolicyKeys);
50
50
  const K = NEON_ENV_VAR_KEYS;
51
51
  const storageCredentialSelected = (desired.preview?.buckets.length ?? 0) > 0 && (selected.has(K.storage.accessKeyId) || selected.has(K.storage.secretAccessKey));
@@ -55,15 +55,16 @@ async function fetchEnvReusingSecrets(config, options) {
55
55
  aiGateway: gatewayCredentialSelected
56
56
  }).filter((key) => selected.has(key));
57
57
  if (secretKeys.length === 0) {
58
- const fetched = await fetchEnvKeys(config, fetchOptions, requested === null ? null : selectedPolicyKeys);
58
+ const fetched = await fetchEnvKeysState(config, fetchOptions, requested === null ? null : selectedPolicyKeys);
59
59
  return {
60
- vars: preferPersisted(toEntries(fetched), source),
60
+ vars: preferPersisted(toEntries(fetched.env), source),
61
61
  credential: {
62
62
  issued: false,
63
63
  keys: [],
64
64
  revoked: [],
65
65
  superseded: []
66
- }
66
+ },
67
+ ...fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {}
67
68
  };
68
69
  }
69
70
  const persisted = readPersistedSecrets(source);
@@ -83,13 +84,15 @@ async function fetchEnvReusingSecrets(config, options) {
83
84
  aiGateway: gatewayCredentialSelected
84
85
  });
85
86
  const keep = reusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);
86
- const fetchKeys = keep ? selectedPolicyKeys.filter((key) => !secretKeys.includes(key)) : selectedPolicyKeys;
87
- const fetched = await fetchEnvKeys(config, {
87
+ const fetchKeys = requested === null ? null : keep ? selectedPolicyKeys.filter((key) => !secretKeys.includes(key)) : selectedPolicyKeys;
88
+ const fetched = await fetchEnvKeysState(config, {
88
89
  ...fetchOptions,
89
90
  branchId: branch.id,
90
- api
91
+ api,
92
+ ...keep && requested === null ? { omitKeys: secretKeys } : {}
91
93
  }, fetchKeys);
92
- const vars = preferPersisted(toEntries(fetched), source);
94
+ const vars = preferPersisted(toEntries(fetched.env), source);
95
+ const unavailable = fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {};
93
96
  if (keep) {
94
97
  for (const key of secretKeys) {
95
98
  const value = source[key];
@@ -102,7 +105,8 @@ async function fetchEnvReusingSecrets(config, options) {
102
105
  keys: secretKeys,
103
106
  revoked: [],
104
107
  superseded: []
105
- }
108
+ },
109
+ ...unavailable
106
110
  };
107
111
  }
108
112
  const ours = /* @__PURE__ */ new Set();
@@ -115,7 +119,8 @@ async function fetchEnvReusingSecrets(config, options) {
115
119
  keys: secretKeys,
116
120
  revoked: revokeSuperseded ? [...ours] : [],
117
121
  superseded: revokeSuperseded ? [] : [...ours]
118
- }
122
+ },
123
+ ...unavailable
119
124
  };
120
125
  }
121
126
  /** Read the branch credential's secrets out of an env source. */
@@ -197,14 +202,7 @@ const apiOptions = (ctx) => ({
197
202
  ...ctx.apiHost ? { apiHost: ctx.apiHost } : {},
198
203
  ...ctx.api ? { api: ctx.api } : {}
199
204
  });
200
- /**
201
- * Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
202
- * Data API, a bucket, the AI Gateway) that the linked remote branch does not
203
- * have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
204
- * to "run without injection" — this is a hard stop: the user's intent (a policy)
205
- * cannot be honored, and silently dropping the secret would be more confusing
206
- * than refusing to start. The fix is to provision the resource first.
207
- */
205
+ /** Policy mismatches hard-stop because running without declared env vars violates user intent. */
208
206
  var DevEnvMismatchError = class extends Error {
209
207
  constructor(..._args) {
210
208
  super(..._args);
@@ -247,12 +245,17 @@ var ServiceNotOnBranchError = class extends Error {
247
245
  * 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
248
246
  * check it against the branch's live state (`plan`); if it declares a resource
249
247
  * the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
250
- * `neonctl deploy`. Otherwise `fetchEnv` evaluates the policy.
248
+ * `neonctl deploy`. Function declarations are excluded from that check: their
249
+ * invocation URLs are derived from the branch connection host, so an undeployed
250
+ * function still gets `NEON_FUNCTION_*_BASE_URL`. Otherwise `fetchEnv` evaluates
251
+ * the policy.
251
252
  * 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
252
253
  * branch's live state (Auth / Data API enablement plus any object-storage
253
254
  * buckets) into a config, then `fetchEnv` resolves what is actually enabled —
254
255
  * so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
255
- * With {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
256
+ * Function invocation URLs are listed live (`functionUrls: "all-live"`) because
257
+ * `pullConfig` cannot round-trip them into `preview.functions`. With
258
+ * {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
256
259
  * `pullConfig` cannot read it back.
257
260
  * 3. otherwise -> throw {@link MissingBranchContextError}.
258
261
  *
@@ -261,12 +264,11 @@ var ServiceNotOnBranchError = class extends Error {
261
264
  */
262
265
  const resolveNeonEnvVars = async (ctx) => {
263
266
  if (ctx.services !== void 0 || ctx.envKeys !== void 0) return await resolveSelectedServices(ctx, ctx.services ?? [], ctx.envKeys ?? []);
264
- const config = await loadNeonConfig(ctx.cwd);
267
+ const config = await loadNeonConfig(ctx);
265
268
  if (config) {
266
269
  if (!ctx.projectId || !ctx.branchId) throw new MissingBranchContextError(`Found a neon.ts but could not resolve the project/branch. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
267
- const envConfig = withoutPreviewFunctions(config);
268
- await assertPolicyMatchesBranch(envConfig, ctx);
269
- return await fetchAndProject(envConfig, ctx);
270
+ await assertPolicyMatchesBranch(withoutPreviewFunctions(config), ctx);
271
+ return await fetchAndProject(config, ctx);
270
272
  }
271
273
  if (ctx.projectId && ctx.branchId) {
272
274
  const pulled = await pullConfig({
@@ -274,7 +276,7 @@ const resolveNeonEnvVars = async (ctx) => {
274
276
  branchId: ctx.branchId,
275
277
  ...apiOptions(ctx)
276
278
  });
277
- if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx);
279
+ if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx, { functionUrls: "all-live" });
278
280
  return await resolveWithImpliedGateway(pulled.config, ctx, {
279
281
  projectId: ctx.projectId,
280
282
  branchId: ctx.branchId
@@ -313,11 +315,12 @@ const withAiGateway = (config) => ({
313
315
  */
314
316
  const resolveWithImpliedGateway = async (config, ctx, branch) => {
315
317
  const unreachable = await credentialsUnreachable(ctx, branch);
316
- if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx);
318
+ if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx, { functionUrls: "all-live" });
317
319
  log.warning(`Could not reach the AI Gateway's credentials, so %s were not resolved. Everything else was. Either this project does not have the AI Gateway, or the call failed — \`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [NEON_ENV_VAR_KEYS.aiGateway.apiKey, NEON_ENV_VAR_KEYS.aiGateway.baseUrl].join(" and "), unreachable);
320
+ const pulled = await fetchAndProject(config, ctx, { functionUrls: "all-live" });
318
321
  return {
319
- ...await fetchAndProject(config, ctx),
320
- skipped: ["ai-gateway"]
322
+ ...pulled,
323
+ skipped: [...pulled.skipped ?? [], "ai-gateway"]
321
324
  };
322
325
  };
323
326
  /**
@@ -354,12 +357,17 @@ const resolveSelectedServices = async (ctx, directServices, envKeys) => {
354
357
  const api = apiFor(ctx);
355
358
  const has = (service) => services.includes(service);
356
359
  const checkAiGateway = has("ai-gateway") && !selectedKeys.includes(NEON_ENV_VAR_KEYS.aiGateway.apiKey);
357
- const [auth, dataApiEnabled, buckets, aiGatewayAvailable] = await Promise.all([
360
+ const [auth, dataApiEnabled, buckets, aiGatewayAvailable, functions] = await Promise.all([
358
361
  has("auth") ? api.getNeonAuth(projectId, branchId) : null,
359
362
  has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
360
363
  has("object-storage") ? api.listBranchBuckets(projectId, branchId) : null,
361
- checkAiGateway ? readAiGatewayAvailable(api, projectId, branchId) : null
364
+ checkAiGateway ? readAiGatewayAvailable(api, projectId, branchId) : null,
365
+ directServices.includes("functions") ? api.listBranchFunctions(projectId, branchId) : null
362
366
  ]);
367
+ const callableFunctions = (functions ?? []).filter((fn) => fn.invocationUrl !== "");
368
+ if (directServices.includes("functions") && callableFunctions.length === 0) throw new ServiceNotOnBranchError(`--service functions: branch ${branchId} has no deployed functions, so there are no NEON_FUNCTION_*_BASE_URL vars to pull. Deploy a function first (\`${getCliName()} deploy\`, or in the Neon Console), or drop functions from --service.`);
369
+ const functionKeys = directServices.includes("functions") ? callableFunctions.map((fn) => functionBaseUrlKey(fn.slug)).sort((left, right) => left.localeCompare(right)) : [];
370
+ const fetchKeys = [.../* @__PURE__ */ new Set([...selectedKeys, ...functionKeys])];
363
371
  const config = configForServices(services, branchId, {
364
372
  authEnabled: auth !== null,
365
373
  dataApiEnabled,
@@ -370,8 +378,12 @@ const resolveSelectedServices = async (ctx, directServices, envKeys) => {
370
378
  envKeys
371
379
  });
372
380
  return await fetchAndProject(config, ctx, {
373
- keys: selectedKeys,
374
- revokeSuperseded: false
381
+ keys: fetchKeys,
382
+ revokeSuperseded: false,
383
+ ...directServices.includes("functions") ? {
384
+ functionUrls: "all-live",
385
+ listedFunctions: callableFunctions
386
+ } : {}
375
387
  });
376
388
  };
377
389
  /**
@@ -395,7 +407,7 @@ const readAiGatewayAvailable = async (api, projectId, branchId) => {
395
407
  await api.listCredentials(projectId, branchId);
396
408
  return true;
397
409
  } catch (error) {
398
- if (isPlatformError(error) && error.code === ErrorCode$1.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
410
+ if (isPlatformError$1(error) && error.code === ErrorCode$1.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
399
411
  throw error;
400
412
  }
401
413
  };
@@ -483,15 +495,7 @@ const resolveDevEnv = async (ctx) => {
483
495
  };
484
496
  }
485
497
  };
486
- /**
487
- * Return the policy with its `preview.functions` removed, so the env path never enumerates
488
- * functions against the Neon API. Functions are local-source-bundled and produce no
489
- * branch-level secrets, so they are irrelevant to env resolution; probing them only risks
490
- * failing the whole resolve (undeployed function, or Functions Preview disabled on the
491
- * project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
492
- * they DO carry env, so they must still be checked and resolved. Returns the config
493
- * unchanged when it declares no functions.
494
- */
498
+ /** Functions are omitted because `plan` treats undeployed functions as resources to create. */
495
499
  const withoutPreviewFunctions = (config) => {
496
500
  const preview = config.preview;
497
501
  if (!preview?.functions) return config;
@@ -507,10 +511,6 @@ const withoutPreviewFunctions = (config) => {
507
511
  * it declares a branch-level resource the branch is missing. Built on `plan` so
508
512
  * it covers every present and future provisionable resource for free: any
509
513
  * `create` action is a resource `neonctl deploy` would provision.
510
- *
511
- * Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
512
- * `plan` probe never enumerates the functions API — an undeployed function, or a project
513
- * without the Functions Preview, must never block local dev or sink env injection.
514
514
  */
515
515
  const assertPolicyMatchesBranch = async (config, ctx) => {
516
516
  const missing = (await plan(config, {
@@ -528,14 +528,23 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
528
528
  * — and functions are excluded (see {@link assertPolicyMatchesBranch}).
529
529
  */
530
530
  const isMissingResource = (change) => change.kind === "service" && change.action === "create" && !change.identifier.startsWith("function:");
531
- const fetchAndProject = async (config, ctx, opts = {}) => fetchEnvReusingSecrets(config, {
532
- projectId: ctx.projectId,
533
- branch: ctx.branchId,
534
- ...apiOptions(ctx),
535
- ...ctx.env ? { env: ctx.env } : {},
536
- ...opts.keys ? { keys: opts.keys } : {},
537
- ...opts.revokeSuperseded === false ? { revokeSuperseded: false } : {}
538
- });
531
+ const fetchAndProject = async (config, ctx, opts = {}) => {
532
+ const result = await fetchEnvReusingSecrets(config, {
533
+ projectId: ctx.projectId,
534
+ branch: ctx.branchId,
535
+ ...apiOptions(ctx),
536
+ ...ctx.env ? { env: ctx.env } : {},
537
+ ...opts.keys ? { keys: opts.keys } : {},
538
+ ...opts.revokeSuperseded === false ? { revokeSuperseded: false } : {},
539
+ ...opts.functionUrls ? { functionUrls: opts.functionUrls } : {},
540
+ ...opts.listedFunctions ? { listedFunctions: opts.listedFunctions } : {}
541
+ });
542
+ return {
543
+ vars: result.vars,
544
+ credential: result.credential,
545
+ ...result.functionUrlsUnavailable ? { skipped: ["functions"] } : {}
546
+ };
547
+ };
539
548
  /**
540
549
  * Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
541
550
  * root. Returns `null` when there is none (the common "no config" case), and
@@ -570,14 +579,17 @@ const looksLikeMissingDependency = (err) => {
570
579
  const text = errorChainText(err);
571
580
  return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
572
581
  };
573
- const loadNeonConfig = async (cwd) => {
582
+ const loadNeonConfig = async (ctx) => {
574
583
  try {
575
- const { config } = await loadConfigFromFile({ cwd });
584
+ const { config } = await loadConfigFromFile({
585
+ cwd: ctx.cwd,
586
+ ...ctx.omitUnsetFunctionEnv ? { unsetFunctionEnv: "omit" } : {}
587
+ });
576
588
  return config;
577
589
  } catch (err) {
578
590
  const message = err instanceof Error ? err.message : String(err);
579
591
  if (/Could not find a Neon config file/i.test(message)) return null;
580
- if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(cwd))}\`, then try again.\nOriginal error: ${message}`);
592
+ if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(ctx.cwd))}\`, then try again.\nOriginal error: ${message}`);
581
593
  throw err;
582
594
  }
583
595
  };