neon 4.18.1 → 4.20.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.
package/README.md CHANGED
@@ -583,7 +583,7 @@ If you'd rather not keep env vars on disk, inject them at runtime instead with `
583
583
 
584
584
  ## Config as code (`config` / `deploy`)
585
585
 
586
- Describe a branch's desired state in a `neon.ts` policy and reconcile it from the CLI — the Neon equivalent of `terraform status` / `plan` / `apply`. A policy splits into a **static** existential set — top-level `auth` / `dataApi` toggles and the beta `preview` block (Functions, buckets, AI Gateway) that decide what _exists_ — and a **dynamic** `branch` closure that tunes each branch (compute settings, TTL, protection, `parent`) based on the branch it's evaluated for (`name`, `isDefault`, …):
586
+ Describe a branch's desired state in a `neon.ts` policy and reconcile it from the CLI — the Neon equivalent of `terraform status` / `plan` / `apply`. A policy splits into a **static** existential set — top-level `auth` / `dataApi` / `aiGateway` / `functions` / `buckets` (the last three still work under deprecated `preview`) that decide what _exists_ — and a **dynamic** `branch` closure that tunes each branch (compute settings, TTL, protection, `parent`) based on the branch it's evaluated for (`name`, `isDefault`, …):
587
587
 
588
588
  ```ts
589
589
  // neon.ts
@@ -610,6 +610,8 @@ export default defineConfig({
610
610
  ? Which Neon services should neon.ts declare? (space to toggle, enter to confirm) ›
611
611
  ◯ Managed Better Auth
612
612
  Authentication with users and sessions stored in Postgres.
613
+ ◯ Data API
614
+ PostgREST-compatible HTTP API. Also declares Auth; the default provider needs it.
613
615
  ◯ Functions
614
616
  Long-running, without timeouts, and closer to your database.
615
617
  ◯ Object Storage
@@ -627,6 +629,9 @@ neon config init
627
629
  # Declare services with no prompt
628
630
  neon config init --services auth,functions,object-storage,ai-gateway
629
631
 
632
+ # Data API also writes auth: true (the default provider requires it)
633
+ neon config init --services data-api
634
+
630
635
  # Repeat the flag instead, and shorten it — every services flag takes all three spellings
631
636
  neon config init -s auth -s functions
632
637
 
@@ -724,7 +729,7 @@ neon config plan --project-id polished-snowflake-12345678 --output json
724
729
  neon deploy --branch my-feature --update-existing
725
730
  ```
726
731
 
727
- 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`.
732
+ Function deploys declared under `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`.
728
733
 
729
734
  Point a domain you already own at a function with `neon function domains` (alias `domain`):
730
735
 
@@ -1,10 +1,10 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
3
  import { declaredNeonServices } from "../config_services.js";
4
- import { C as resolveBranchPolicy, S as policyEnvKeys, _ as defaultStorageCredential, a as envKeysForSelection, b as isFunctionBaseUrlKey, d as servicesForEnvKeys, f as NEON_ENV_VAR_KEYS, g as defaultAiGatewayCredential, h as credentialName, i as ENV_PULL_SERVICES, m as credentialEnvKeys, p as createApiFromOptions, u as serviceForEnvKey, v as fetchEnvKeysState, w as toEntries, x as isLiveCredential, y as functionBaseUrlKey } from "./env_services-CbARRnq9.js";
4
+ import { C as resolveBranchPolicy, S as policyEnvKeys, _ as defaultStorageCredential, a as envKeysForSelection, b as isFunctionBaseUrlKey, d as servicesForEnvKeys, f as NEON_ENV_VAR_KEYS, g as defaultAiGatewayCredential, h as credentialName, i as ENV_PULL_SERVICES, m as credentialEnvKeys, p as createApiFromOptions, u as serviceForEnvKey, v as fetchEnvKeysState, w as toEntries, x as isLiveCredential, y as functionBaseUrlKey } from "./env_services-DlXECBPi.js";
5
5
  import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
6
+ import { ErrorCode, createNeonApiFromOptions, isPlatformError, loadConfigFromFile } from "@neon/config";
6
7
  import { credentialScopesSatisfied } from "@neon/config/v1";
7
- import { ErrorCode as ErrorCode$1, createNeonApiFromOptions as createNeonApiFromOptions$1, isPlatformError as isPlatformError$1, loadConfigFromFile } from "@neon/config";
8
8
  import { plan, pullConfig } from "@neon/config-runtime";
9
9
  //#region ../../internals/env-core/dist/reuse-secrets.js
10
10
  /**
@@ -375,7 +375,7 @@ const resolveClaimableNeonEnvVars = async (ctx) => {
375
375
  * buckets) into a config, then `fetchEnv` resolves what is actually enabled —
376
376
  * so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
377
377
  * Function invocation URLs are listed live (`functionUrls: "all-live"`) because
378
- * `pullConfig` cannot round-trip them into `preview.functions`. With
378
+ * `pullConfig` cannot round-trip them into `functions`. With
379
379
  * {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
380
380
  * `pullConfig` cannot read it back.
381
381
  * 3. otherwise -> throw {@link MissingBranchContextError}.
@@ -394,7 +394,7 @@ const resolveNeonEnvVars = async (ctx) => {
394
394
  const config = await loadNeonConfig(ctx);
395
395
  if (config) {
396
396
  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.`);
397
- await assertPolicyMatchesBranch(withoutPreviewFunctions(config), ctx);
397
+ await assertPolicyMatchesBranch(withoutDeclaredFunctions(config), ctx);
398
398
  return await fetchAndProject(config, ctx);
399
399
  }
400
400
  if (ctx.projectId && ctx.branchId) {
@@ -465,7 +465,7 @@ const credentialsUnreachable = async (ctx, branch) => {
465
465
  }
466
466
  };
467
467
  /** The adapter for direct branch reads: the injected one in tests, else built from options. */
468
- const apiFor = (ctx) => ctx.api ?? createNeonApiFromOptions$1("neon env", {
468
+ const apiFor = (ctx) => ctx.api ?? createNeonApiFromOptions("neon env", {
469
469
  ...ctx.apiKey ? { apiKey: ctx.apiKey } : {},
470
470
  ...ctx.apiHost ? { apiHost: ctx.apiHost } : {}
471
471
  });
@@ -535,7 +535,7 @@ const readAiGatewayAvailable = async (api, projectId, branchId) => {
535
535
  await api.listCredentials(projectId, branchId);
536
536
  return true;
537
537
  } catch (error) {
538
- if (isPlatformError$1(error) && error.code === ErrorCode$1.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
538
+ if (isPlatformError(error) && error.code === ErrorCode.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
539
539
  throw error;
540
540
  }
541
541
  };
@@ -576,16 +576,14 @@ const configForServices = (services, branchId, branch, selection) => {
576
576
  if (branch.dataApiEnabled === false) notOnBranch("data-api", "Data API integration");
577
577
  config.dataApi = true;
578
578
  }
579
- const preview = {};
580
579
  if (services.includes("object-storage")) {
581
580
  if (branch.buckets.length === 0) notOnBranch("object-storage", "object-storage buckets");
582
- preview.buckets = Object.fromEntries(branch.buckets.map((bucket) => [bucket.name, { access: bucket.accessLevel }]));
581
+ config.buckets = Object.fromEntries(branch.buckets.map((bucket) => [bucket.name, { access: bucket.accessLevel }]));
583
582
  }
584
583
  if (services.includes("ai-gateway")) {
585
584
  if (!branch.aiGatewayAvailable) aiGatewayUnavailable();
586
- preview.aiGateway = true;
585
+ config.aiGateway = true;
587
586
  }
588
- if (Object.keys(preview).length > 0) config.preview = preview;
589
587
  return config;
590
588
  };
591
589
  /**
@@ -624,15 +622,15 @@ const resolveDevEnv = async (ctx) => {
624
622
  }
625
623
  };
626
624
  /** Functions are omitted because `plan` treats undeployed functions as resources to create. */
627
- const withoutPreviewFunctions = (config) => {
628
- const preview = config.preview;
629
- if (!preview?.functions) return config;
630
- const previewWithoutFunctions = { ...preview };
631
- delete previewWithoutFunctions.functions;
632
- return {
633
- ...config,
634
- preview: previewWithoutFunctions
635
- };
625
+ const withoutDeclaredFunctions = (config) => {
626
+ const next = { ...config };
627
+ if (next.functions !== void 0) delete next.functions;
628
+ if (next.preview?.functions) {
629
+ const previewWithoutFunctions = { ...next.preview };
630
+ delete previewWithoutFunctions.functions;
631
+ next.preview = previewWithoutFunctions;
632
+ }
633
+ return next;
636
634
  };
637
635
  /**
638
636
  * Tier-1 guard. Dry-run the policy against the branch's live state and stop if
@@ -185,7 +185,7 @@ async function fetchEnvKeysState(config, options, keys) {
185
185
  let storage = null;
186
186
  if (wantsStorage) {
187
187
  storage = await api.getProjectBranchStorage(projectId, branch.id);
188
- 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: {
188
+ if (!storage) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy declares object storage (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 buckets."].join(" "), { details: {
189
189
  projectId,
190
190
  branchId: branch.id
191
191
  } });
@@ -77,6 +77,7 @@ const handler = async (props) => {
77
77
  const failure = created && !policyApplied ? await applyPolicyOrDescribeFailure({
78
78
  projectId,
79
79
  branchId,
80
+ branchName,
80
81
  ...props.apiKey ? { apiKey: props.apiKey } : {},
81
82
  ...props.apiHost ? { apiHost: props.apiHost } : {},
82
83
  ...props.color !== void 0 ? { color: props.color } : {},
@@ -21,8 +21,8 @@ import { zipBundle } from "../utils/zip.js";
21
21
  import { autoPullEnvAfterPin } from "./env.js";
22
22
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
23
23
  import { dirname, join } from "node:path";
24
+ import { packagesToStage, previewGaWarningForConfig, resolveConfig } from "@neon/config";
24
25
  import chalk from "chalk";
25
- import { packagesToStage, resolveConfig } from "@neon/config";
26
26
  import { PushConflictError, apply, assertZipWithinLimits, bundleAsIs, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, loadConfigFromFile as loadConfigFromFile$1, plan, resolveEsbuildEntry, traceNativePackages, zipFunctionBundle } from "@neon/config-runtime";
27
27
  //#region src/commands/config.ts
28
28
  var config_exports = /* @__PURE__ */ __exportAll({
@@ -161,7 +161,7 @@ const resolveServices = async (props) => {
161
161
  return pickServicesInteractively();
162
162
  };
163
163
  /**
164
- * Write the hello-world handler the scaffolded `preview.functions` entry points at. An
164
+ * Write the hello-world handler the scaffolded `functions` entry points at. An
165
165
  * existing `hello.ts` is left alone: the declared function keeps pointing at it, which is the
166
166
  * better outcome than overwriting a file the user wrote.
167
167
  */
@@ -389,10 +389,76 @@ const planCmd = async (props) => {
389
389
  branchId
390
390
  });
391
391
  };
392
+ const warnDeprecatedPreview = (config, target) => {
393
+ const message = previewGaWarningForConfig(config, target);
394
+ if (message) log.warning("%s", message);
395
+ };
396
+ const warningTargetFromResolved = (branch) => ({
397
+ name: branch.branchName,
398
+ id: branch.branchId,
399
+ exists: true,
400
+ ...branch.isDefault !== void 0 ? { isDefault: branch.isDefault } : {},
401
+ ...branch.isProtected !== void 0 ? { isProtected: branch.isProtected } : {},
402
+ ...branch.parentId ? { parentId: branch.parentId } : {},
403
+ ...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
404
+ });
405
+ const warningTargetForExistingBranch = async (props) => {
406
+ if (props.runtimeApi) {
407
+ const branch = (await props.runtimeApi.listBranches(props.projectId)).find((listed) => listed.id === props.branchId);
408
+ if (branch) return {
409
+ name: branch.name,
410
+ id: branch.id,
411
+ exists: true,
412
+ isDefault: branch.isDefault,
413
+ isProtected: branch.protected,
414
+ ...branch.parentId ? { parentId: branch.parentId } : {},
415
+ ...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
416
+ };
417
+ }
418
+ if (props.apiKey) {
419
+ const { data } = await getApiClient({
420
+ apiKey: props.apiKey,
421
+ ...props.apiHost ? { apiHost: props.apiHost } : {}
422
+ }).listProjectBranches({ projectId: props.projectId });
423
+ const found = data.branches.find((listed) => listed.id === props.branchId);
424
+ if (found) return {
425
+ name: found.name ?? props.branchName ?? props.branchId,
426
+ id: found.id,
427
+ exists: true,
428
+ isDefault: found.default === true,
429
+ isProtected: found.protected === true,
430
+ ...found.parent_id ? { parentId: found.parent_id } : {},
431
+ ...found.expires_at ? { expiresAt: found.expires_at } : {}
432
+ };
433
+ }
434
+ return {
435
+ name: props.branchName ?? props.branchId,
436
+ id: props.branchId,
437
+ exists: true,
438
+ isDefault: false
439
+ };
440
+ };
441
+ const warnDeprecatedPreviewOnCreate = async (config, props) => {
442
+ try {
443
+ const listed = await warningTargetForExistingBranch(props);
444
+ warnDeprecatedPreview(config, {
445
+ ...listed,
446
+ exists: false
447
+ });
448
+ } catch {
449
+ warnDeprecatedPreview(config, {
450
+ name: props.branchName,
451
+ id: props.branchId,
452
+ exists: false,
453
+ isDefault: false
454
+ });
455
+ }
456
+ };
392
457
  const applyCmd = async (props) => {
393
458
  const config = await loadConfig(props);
394
459
  const branch = await resolveBranchRef(props);
395
460
  announceTargetBranch(props, branch, "Applying to branch");
461
+ warnDeprecatedPreview(config, warningTargetFromResolved(branch));
396
462
  const branchId = branch.branchId;
397
463
  if (utilizedServices(config).includes("AI Gateway")) await assertAiGatewayProvisionable({
398
464
  apiClient: props.apiClient,
@@ -429,7 +495,7 @@ const applyCmd = async (props) => {
429
495
  * the plan/apply table. Postgres is always present (every branch has it); the rest are listed
430
496
  * only when the policy declares them. This deliberately surfaces services that produce **no**
431
497
  * plan step — notably the AI Gateway, which is always available and only needs a scoped branch
432
- * credential (not a provisioning step) — so adding `preview.aiGateway` to a neon.ts isn't
498
+ * credential (not a provisioning step) — so adding `aiGateway` to a neon.ts isn't
433
499
  * mistaken for being silently dropped. Service enablement is static top-level config (it never
434
500
  * lives in the per-branch closure), so reading it straight off `config` is accurate.
435
501
  */
@@ -547,6 +613,18 @@ const applyPolicyOnCreate = async (props) => {
547
613
  if (/Could not find a Neon config file/i.test(message)) return;
548
614
  throw err;
549
615
  }
616
+ try {
617
+ warnDeprecatedPreview(config, await warningTargetForExistingBranch({
618
+ projectId: props.projectId,
619
+ branchId: props.branchId,
620
+ ...props.branchName ? { branchName: props.branchName } : {},
621
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
622
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
623
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
624
+ }));
625
+ } catch {
626
+ warnDeprecatedPreview(config);
627
+ }
550
628
  await assertAiGatewayProvisionableFromCreds({
551
629
  projectId: props.projectId,
552
630
  ...props.apiKey ? { apiKey: props.apiKey } : {},
@@ -634,10 +712,26 @@ const createBranchFromPolicyOnCheckout = async (props) => {
634
712
  });
635
713
  log.info("Created branch %s (%s) from neon.ts policy.", branchName, branchId);
636
714
  logPolicyResult(result, { color: props.color !== false });
715
+ await warnDeprecatedPreviewOnCreate(config, {
716
+ projectId: props.projectId,
717
+ branchId,
718
+ branchName,
719
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
720
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
721
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
722
+ });
637
723
  return { branchId };
638
724
  } catch (err) {
639
725
  if (isPartialBranchCreateError(err)) {
640
726
  log.info("Created branch %s (%s) from neon.ts policy.", err.branchName, err.branchId);
727
+ await warnDeprecatedPreviewOnCreate(config, {
728
+ projectId: props.projectId,
729
+ branchId: err.branchId,
730
+ branchName: err.branchName,
731
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
732
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
733
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
734
+ });
641
735
  return {
642
736
  branchId: err.branchId,
643
737
  policyFailure: err.reason
@@ -3,10 +3,10 @@ import { log } from "../log.js";
3
3
  import { isClaimableEnvTarget } from "../claimable/state.js";
4
4
  import { getCliName } from "../utils/cli_name.js";
5
5
  import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
6
- import { y as functionBaseUrlKey } from "../_chunks/env_services-CbARRnq9.js";
6
+ import { y as functionBaseUrlKey } from "../_chunks/env_services-DlXECBPi.js";
7
7
  import { branchIdResolve } from "../utils/enrichers.js";
8
8
  import { bundleEntry } from "../utils/esbuild.js";
9
- import { a as resolveDevEnv } from "../_chunks/env-B75xufe_.js";
9
+ import { a as resolveDevEnv } from "../_chunks/env-DRpFX-_d.js";
10
10
  import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
11
11
  import { resolveWatchInputs } from "../dev/inputs.js";
12
12
  import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -149,9 +149,9 @@ const runSingleSource = async (props) => {
149
149
  const runFromConfig = async (props) => {
150
150
  const branchId = await resolveBranchId(props);
151
151
  const resolved = await resolveFunctionsFromConfig(process.cwd());
152
- if (resolved === null) throw new Error("No --source given and no neon.ts found. Pass --source <path> to run a single function, or add a neon.ts that declares functions under `preview.functions`.");
152
+ if (resolved === null) throw new Error("No --source given and no neon.ts found. Pass --source <path> to run a single function, or add a neon.ts that declares functions under `functions`.");
153
153
  const { configPath, functions } = resolved;
154
- if (functions.length === 0) throw new Error("neon.ts has no functions to serve. Add at least one under `preview.functions`, or pass --source <path>.");
154
+ if (functions.length === 0) throw new Error("neon.ts has no functions to serve. Add at least one under `functions`, or pass --source <path>.");
155
155
  const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
156
156
  reportDevCredential(credential);
157
157
  const units = await planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
@@ -5,11 +5,11 @@ import { isClaimableEnvTarget } from "../claimable/state.js";
5
5
  import { getCliName } from "../utils/cli_name.js";
6
6
  import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
7
7
  import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
8
- import { a as envKeysForSelection, b as isFunctionBaseUrlKey, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, o as envPullFlagValue, r as ENV_PULL_KEY_HELP } from "../_chunks/env_services-CbARRnq9.js";
8
+ import { a as envKeysForSelection, b as isFunctionBaseUrlKey, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, o as envPullFlagValue, r as ENV_PULL_KEY_HELP } from "../_chunks/env_services-DlXECBPi.js";
9
9
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
10
10
  import { announceTargetBranch } from "../utils/branch_notice.js";
11
11
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
12
- import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-B75xufe_.js";
12
+ import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-DRpFX-_d.js";
13
13
  import { existsSync } from "node:fs";
14
14
  import chalk from "chalk";
15
15
  //#region src/commands/env.ts
@@ -1,3 +1,4 @@
1
+ import { authoredAiGateway, authoredBuckets, authoredFunctions } from "@neon/config";
1
2
  //#region src/config_services.ts
2
3
  const isToggleEnabled = (toggle) => {
3
4
  if (toggle === void 0) return false;
@@ -9,9 +10,9 @@ const declaredNeonServices = (config) => {
9
10
  const services = [];
10
11
  if (isToggleEnabled(config.auth)) services.push("auth");
11
12
  if (isToggleEnabled(config.dataApi)) services.push("data-api");
12
- if (Object.keys(config.preview?.buckets ?? {}).length > 0) services.push("object-storage");
13
- if (Object.keys(config.preview?.functions ?? {}).length > 0) services.push("functions");
14
- if (isToggleEnabled(config.preview?.aiGateway)) services.push("ai-gateway");
13
+ if (Object.keys(authoredBuckets(config) ?? {}).length > 0) services.push("object-storage");
14
+ if (Object.keys(authoredFunctions(config) ?? {}).length > 0) services.push("functions");
15
+ if (isToggleEnabled(authoredAiGateway(config))) services.push("ai-gateway");
15
16
  return services;
16
17
  };
17
18
  const dataApiSettingsToSnake = (settings) => {
@@ -15,24 +15,21 @@ const REQUIRED_PACKAGES = [CONFIG_PACKAGE, ENV_PACKAGE];
15
15
  /**
16
16
  * The services `config init` can declare in the `neon.ts` it scaffolds — the subset of
17
17
  * {@link NEON_SERVICES} a policy has a field for. {@link renderNeonConfig} owns the mapping
18
- * from these names to the `neon.ts` fields (`aiGateway`, `buckets`).
18
+ * from these names to the `neon.ts` fields (`aiGateway`, `functions`, `buckets`, `dataApi`).
19
19
  *
20
- * Postgres is absent because every branch has it, so there is nothing to declare. `data-api`
21
- * is absent because enabling it with the default `authProvider: "neon"` requires `auth` — a
22
- * pairing the picker would have to enforce rather than offer.
20
+ * Postgres is absent because every branch has it, so there is nothing to declare. Selecting
21
+ * `data-api` also writes `auth: true`: the default Data API provider requires Neon Auth, and
22
+ * `defineConfig` type-errors the file without it.
23
23
  */
24
- const CONFIG_INIT_SERVICES = NEON_SERVICES.filter((service) => service !== "postgres" && service !== "data-api");
24
+ const CONFIG_INIT_SERVICES = NEON_SERVICES.filter((service) => service !== "postgres");
25
25
  /**
26
26
  * What `config init --services none` produces. One constant because it is both the help text
27
27
  * and what tells the parser `none` is a value here — passing the literal at each call site
28
28
  * lets the two drift into documenting something the parser does not accept.
29
29
  */
30
30
  const CONFIG_INIT_NONE_MEANS = "the bare starter policy";
31
- /** Why the two a policy cannot declare are not selectable, for the refusal message. */
32
- const CONFIG_INIT_UNAVAILABLE = {
33
- postgres: "every branch has Postgres, so a policy has nothing to declare for it",
34
- "data-api": "enabling it with the default provider requires auth, so declare auth here and turn the Data API on with `neon data-api create`"
35
- };
31
+ /** Why a policy cannot declare a real Neon service, for the refusal message. */
32
+ const CONFIG_INIT_UNAVAILABLE = { postgres: "every branch has Postgres, so a policy has nothing to declare for it" };
36
33
  /** Slug, display name, and source path of the function scaffolded for `functions`. */
37
34
  const FUNCTION_SLUG = "hello";
38
35
  const FUNCTION_NAME = "Hello World";
@@ -56,27 +53,33 @@ const block = (level, key, body) => [
56
53
  ...body,
57
54
  ...at(level, "},")
58
55
  ];
59
- /** The `preview` block for the selected services, or "" when none of them is a preview feature. */
60
- const renderPreview = (services) => {
56
+ /** GA service keys for the selected init services, or "" when none of them is selected. */
57
+ const renderGaServices = (services) => {
61
58
  const lines = [];
62
- if (services.includes("ai-gateway")) lines.push(...at(2, "aiGateway: true,"));
63
- if (services.includes("functions")) lines.push(...block(2, "functions", [...at(3, `${FUNCTION_SLUG}: { name: "${FUNCTION_NAME}", source: "./${FUNCTION_FILENAME}" },`)]));
64
- if (services.includes("object-storage")) lines.push(...block(2, "buckets", [...at(3, `// "private" is the default; use "public_read" for anonymous reads`, `${BUCKET_NAME}: { access: "private" },`)]));
59
+ if (services.includes("ai-gateway")) lines.push(...at(1, "aiGateway: true,"));
60
+ if (services.includes("functions")) lines.push(...block(1, "functions", [...at(2, `${FUNCTION_SLUG}: { name: "${FUNCTION_NAME}", source: "./${FUNCTION_FILENAME}" },`)]));
61
+ if (services.includes("object-storage")) lines.push(...block(1, "buckets", [...at(2, `// "private" is the default; use "public_read" for anonymous reads`, `${BUCKET_NAME}: { access: "private" },`)]));
65
62
  if (lines.length === 0) return "";
66
- return `${block(1, "preview", lines).join("\n")}\n`;
63
+ return `${lines.join("\n")}\n`;
67
64
  };
68
65
  /**
69
66
  * Render the `neon.ts` policy `config init` writes. With no services this is the starter
70
- * policy — an explicit `auth: false`, no `preview` block, and a `branch` closure that gives
67
+ * policy — an explicit `auth: false`, no service keys beyond that, and a `branch` closure that gives
71
68
  * new non-default branches a 7-day TTL — so the picker's "skip everything" answer and a
72
69
  * non-interactive run produce the identical file.
70
+ *
71
+ * `data-api` writes `dataApi: true` and forces `auth: true`. The default provider is Neon
72
+ * Auth; omitting auth is a `defineConfig` type error rather than a deploy-time surprise.
73
73
  */
74
- const renderNeonConfig = (services) => `import { defineConfig } from "${CONFIG_PACKAGE}/v1";
74
+ const renderNeonConfig = (services) => {
75
+ const auth = services.includes("auth") || services.includes("data-api");
76
+ const dataApi = services.includes("data-api") ? " dataApi: true,\n" : "";
77
+ return `import { defineConfig } from "${CONFIG_PACKAGE}/v1";
75
78
 
76
79
  export default defineConfig({
77
80
  // Declare your Neon services here
78
- auth: ${services.includes("auth")},
79
- ${renderPreview(services)} // Branch policy: per-branch tuning
81
+ auth: ${auth},
82
+ ${dataApi}${renderGaServices(services)} // Branch policy: per-branch tuning
80
83
  branch: (branch) => {
81
84
  if (branch.isDefault) {
82
85
  // Default branch: no overrides, uses project defaults
@@ -92,6 +95,7 @@ ${renderPreview(services)} // Branch policy: per-branch tuning
92
95
  },
93
96
  });
94
97
  `;
98
+ };
95
99
  /** Render a scalar the way it has to appear in TypeScript source. */
96
100
  const renderScalar = (value) => typeof value === "string" ? `"${value}"` : String(value);
97
101
  /**
@@ -121,15 +125,15 @@ const renderSeededBranch = (view) => {
121
125
  ...at(1, "}),")
122
126
  ].join("\n")}\n`;
123
127
  };
124
- /** The `preview` block for a policy seeded from live state. */
125
- const renderSeededPreview = (view, branchName) => {
128
+ /** GA service keys for a policy seeded from live state. */
129
+ const renderSeededGaServices = (view, branchName) => {
126
130
  const lines = [];
127
131
  const buckets = Object.entries(view.preview?.buckets ?? {});
128
- if (buckets.length > 0) lines.push(...block(2, "buckets", buckets.flatMap(([name, bucket]) => at(3, `${renderKey(name)}: { access: "${bucket.access}" },`))));
132
+ if (buckets.length > 0) lines.push(...block(1, "buckets", buckets.flatMap(([name, bucket]) => at(2, `${renderKey(name)}: { access: "${bucket.access}" },`))));
129
133
  const functions = Object.entries(view.preview?.functions ?? {});
130
- if (functions.length > 0) lines.push(...at(2, `// ${branchName} has ${functions.length} deployed function${functions.length === 1 ? "" : "s"}.`, "// Declaring one needs the local source path, which the branch does not know:", "// functions: {", ...functions.map(([slug, fn]) => `// ${renderKey(slug)}: { name: "${fn.name}", source: "./${slug}.ts" },`), "// },"));
134
+ if (functions.length > 0) lines.push(...at(1, `// ${branchName} has ${functions.length} deployed function${functions.length === 1 ? "" : "s"}.`, "// Declaring one needs the local source path, which the branch does not know:", "// functions: {", ...functions.map(([slug, fn]) => `// ${renderKey(slug)}: { name: "${fn.name}", source: "./${slug}.ts" },`), "// },"));
131
135
  if (lines.length === 0) return "";
132
- return `${block(1, "preview", lines).join("\n")}\n`;
136
+ return `${lines.join("\n")}\n`;
133
137
  };
134
138
  /**
135
139
  * Render a `neon.ts` from a branch's live state (`config init --from-branch`).
@@ -148,16 +152,16 @@ const renderSeededPreview = (view, branchName) => {
148
152
  */
149
153
  const renderNeonConfigFromView = (view, branchName) => {
150
154
  const services = [view.auth ? " auth: true," : "", view.dataApi ? " dataApi: true," : ""].filter((line) => line !== "");
151
- const preview = renderSeededPreview(view, branchName);
155
+ const gaServices = renderSeededGaServices(view, branchName);
152
156
  const branch = renderSeededBranch(view);
153
- if (services.length === 0 && preview === "" && branch === "") return {
157
+ if (services.length === 0 && gaServices === "" && branch === "") return {
154
158
  source: renderNeonConfig([]),
155
159
  seeded: false
156
160
  };
157
161
  const protectedNote = view.branch?.protected ? `// ${branchName} is protected on Neon. Not declared here: a policy \`protected\` would\n// apply to every branch this policy is applied to.\n` : "";
158
162
  const body = [
159
163
  ...services,
160
- ...preview === "" ? [] : [preview.trimEnd()],
164
+ ...gaServices === "" ? [] : [gaServices.trimEnd()],
161
165
  ...branch === "" ? [] : [branch.trimEnd()]
162
166
  ].join("\n");
163
167
  return {
@@ -165,7 +169,7 @@ const renderNeonConfigFromView = (view, branchName) => {
165
169
 
166
170
  // Seeded by \`neon config init --from-branch\` from ${branchName}.
167
171
  // The AI Gateway is not readable from a branch (always available, credential-gated), so add
168
- // \`preview: { aiGateway: true }\` if the policy should declare it.
172
+ // \`aiGateway: true\` if the policy should declare it.
169
173
  ${protectedNote}export default defineConfig({
170
174
  ${body}
171
175
  });
package/dist/dev/env.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-B75xufe_.js";
1
+ import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-DRpFX-_d.js";
2
2
  export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, dropClaimableUnsupported, resolveDevEnv, resolveNeonEnvVars };
@@ -1,2 +1,2 @@
1
- import { a as envKeysForSelection, c as ownedEnvServiceKeys, d as servicesForEnvKeys, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, n as ENV_PULL_KEYS, o as envPullFlagValue, r as ENV_PULL_KEY_HELP, s as envServiceKeys, t as BRANCH_ENV_KEY, u as serviceForEnvKey } from "./_chunks/env_services-CbARRnq9.js";
1
+ import { a as envKeysForSelection, c as ownedEnvServiceKeys, d as servicesForEnvKeys, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, n as ENV_PULL_KEYS, o as envPullFlagValue, r as ENV_PULL_KEY_HELP, s as envServiceKeys, t as BRANCH_ENV_KEY, u as serviceForEnvKey } from "./_chunks/env_services-DlXECBPi.js";
2
2
  export { BRANCH_ENV_KEY, ENV_PULL_KEYS, ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, envServiceKeys, ownedEnvServiceKeys, parseEnvPullKeys, serviceForEnvKey, servicesForEnvKeys };
@@ -1,7 +1,7 @@
1
1
  import { log } from "../log.js";
2
2
  //#region src/utils/ai_gateway_notice.ts
3
3
  /**
4
- * Friendly guidance shown when a branch enables the AI Gateway (`preview.aiGateway`).
4
+ * Friendly guidance shown when a branch enables the AI Gateway (`aiGateway`).
5
5
  *
6
6
  * The gateway is credential-gated, not provisioned: enabling it always mints a working
7
7
  * branch credential, so `apply` / `checkout` / `env pull` succeed regardless of plan. The
@@ -50,7 +50,7 @@ const hasFlagshipModels = (modelIds) => modelIds.some((id) => id.includes("opus"
50
50
  * gateway won't serve model requests until the account is on a paid plan. `upgradeUrl` is the
51
51
  * org-scoped Console billing page (see {@link aiGatewayUpgradeUrl}).
52
52
  */
53
- const freePlanBlockMessage = (upgradeUrl) => `This neon.ts enables the AI Gateway, which isn't available on the Free plan — the gateway won't serve model requests. Upgrade to a paid plan and re-run, or remove \`preview.aiGateway\` from neon.ts. Upgrade here: ${upgradeUrl}`;
53
+ const freePlanBlockMessage = (upgradeUrl) => `This neon.ts enables the AI Gateway, which isn't available on the Free plan — the gateway won't serve model requests. Upgrade to a paid plan and re-run, or remove \`aiGateway\` from neon.ts. Upgrade here: ${upgradeUrl}`;
54
54
  /**
55
55
  * Build the AI Gateway courtesy notice for an account's plan and (optionally) its live model
56
56
  * catalog, or `null` when nothing needs saying.
@@ -30,13 +30,20 @@ const resolveBranchRef = async (props) => {
30
30
  const branch = typeof props.branch === "string" ? props.branch : props.id;
31
31
  const { data } = await props.apiClient.listProjectBranches({ projectId: props.projectId });
32
32
  const branches = data.branches;
33
+ const listingFields = (listed) => ({
34
+ isDefault: listed.default === true,
35
+ isProtected: listed.protected === true,
36
+ ...listed.parent_id ? { parentId: listed.parent_id } : {},
37
+ ...listed.expires_at ? { expiresAt: listed.expires_at } : {}
38
+ });
33
39
  if (branch) {
34
40
  const ref = branch.toString();
35
41
  const found = looksLikeBranchId(ref) ? branches.find((b) => b.id === ref) : branches.find((b) => b.name === ref);
36
42
  if (found) return {
37
43
  branchId: found.id,
38
44
  branchName: found.name ?? found.id,
39
- usedDefault: false
45
+ usedDefault: false,
46
+ ...listingFields(found)
40
47
  };
41
48
  if (looksLikeBranchId(ref)) return {
42
49
  branchId: ref,
@@ -50,7 +57,8 @@ const resolveBranchRef = async (props) => {
50
57
  return {
51
58
  branchId: defaultBranch.id,
52
59
  branchName: defaultBranch.name ?? defaultBranch.id,
53
- usedDefault: true
60
+ usedDefault: true,
61
+ ...listingFields(defaultBranch)
54
62
  };
55
63
  };
56
64
  const resolveSingleDatabase = async (props) => {
@@ -12,6 +12,11 @@ const CHOICES = [
12
12
  title: "Managed Better Auth",
13
13
  description: "Authentication with users and sessions stored in Postgres."
14
14
  },
15
+ {
16
+ value: "data-api",
17
+ title: "Data API",
18
+ description: "PostgREST-compatible HTTP API. Also declares Auth; the default provider needs it."
19
+ },
15
20
  {
16
21
  value: "functions",
17
22
  title: "Functions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.18.1",
3
+ "version": "4.20.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config-runtime": "1.4.1",
66
+ "@neon/config-runtime": "1.5.0",
67
67
  "@neon/sdk": "6.0.0",
68
- "@neon/config": "1.5.1"
68
+ "@neon/config": "1.6.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -98,9 +98,9 @@
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
100
  "@neon-internals/cli-core": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.18",
101
102
  "@neon/e2e-harness": "0.0.0",
102
- "@neon-internals/env-core": "0.0.17",
103
- "@neon/functions": "0.10.0"
103
+ "@neon/functions": "0.11.0"
104
104
  },
105
105
  "publishConfig": {
106
106
  "access": "public",