@neondatabase/config 1.5.1 → 1.6.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
@@ -22,17 +22,14 @@ export default defineConfig({
22
22
  // Static: what *exists* on every branch. GA service toggles drive the typed env.
23
23
  auth: true,
24
24
  dataApi: true,
25
- // Beta (Preview) features, keyed by slug / name.
26
- preview: {
27
- functions: {
28
- hello: {
29
- name: "Hello",
30
- source: "./functions/hello.ts",
31
- triggers: [
32
- { type: "schedule", name: "hourly", cron: "0 * * * *" },
33
- ],
34
- customDomains: ["docs.example.com"], // default branch only
35
- },
25
+ functions: {
26
+ hello: {
27
+ name: "Hello",
28
+ source: "./functions/hello.ts",
29
+ triggers: [
30
+ { type: "schedule", name: "hourly", cron: "0 * * * *" },
31
+ ],
32
+ customDomains: ["docs.example.com"], // default branch only
36
33
  },
37
34
  },
38
35
  // Dynamic: per-branch tuning only. Cannot add/remove services or functions.
@@ -45,8 +42,8 @@ export default defineConfig({
45
42
 
46
43
  A policy is split into a **static** existential set and a **dynamic** `branch` closure:
47
44
 
48
- - **Static top-level** — `auth` / `dataApi` (GA service toggles) and the beta `preview` block (`aiGateway`, `functions` keyed by slug, `buckets` keyed by name). Because this is static, the secret set is known at the type level, so `parseEnv` / `fetchEnv` from `@neon/env` return an exact `NeonEnv`. Function source, env, and triggers apply on every branch. `customDomains` is default-branch-only ([details](#default-branch-only-fields)).
49
- - **`branch` closure** — receives a **read-only descriptor** (`BranchTarget`) of the branch being evaluated (`name`, `id`, `exists`, `isDefault`, `isProtected`, `parentId`, `expiresAt`) and returns per-branch *tuning*: `parent`, `ttl`, `protected`, `postgres.computeSettings`, per-function `runtime`, and per-function `customDomains`. Function memory is fixed at `2048` MiB for now and is not user-configurable. It runs both against existing branches and during pre-create evaluation (`exists: false`). It **cannot** change which services or functions exist — that is what keeps the static secret set sound.
45
+ - **Static top-level** — `auth` / `dataApi` / `aiGateway` / `functions` (keyed by slug) / `buckets` (keyed by name). The same keys still work under `preview` (deprecated; `neon config apply` / `deploy` warn that they can be lifted). Because this is static, the secret set is known at the type level, so `parseEnv` / `fetchEnv` from `@neon/env` return an exact `NeonEnv`. Function source, env, and triggers apply on every branch. `customDomains` is default-branch-only ([details](#default-branch-only-fields)).
46
+ - **`branch` closure** — receives a **read-only descriptor** (`BranchTarget`) of the branch being evaluated (`name`, `id`, `exists`, `isDefault`, `isProtected`, `parentId`, `expiresAt`) and returns per-branch *tuning*: `parent`, `ttl`, `protected`, `postgres.computeSettings`, per-function `runtime` (`functions` on the returned object, or deprecated `preview.functions`), and per-function `customDomains`. Function memory is fixed at `2048` MiB for now and is not user-configurable. It runs both against existing branches and during pre-create evaluation (`exists: false`). It **cannot** change which services or functions exist — that is what keeps the static secret set sound.
50
47
 
51
48
  Service toggles accept `true` / `{}` / `{ enabled: true }` (enabled) and `false` / `{ enabled: false }` (disabled). Function slugs (record keys) must match `^[a-z0-9]{1,20}$`.
52
49
 
@@ -56,13 +53,11 @@ A hostname is globally unique, so a child checkout would 409 if it inherited the
56
53
 
57
54
  ```ts
58
55
  export default defineConfig({
59
- preview: {
60
- functions: {
61
- hello: {
62
- name: "Hello",
63
- source: "./functions/hello.ts",
64
- customDomains: ["docs.example.com"],
65
- },
56
+ functions: {
57
+ hello: {
58
+ name: "Hello",
59
+ source: "./functions/hello.ts",
60
+ customDomains: ["docs.example.com"],
66
61
  },
67
62
  },
68
63
  });
@@ -80,10 +75,8 @@ export default defineConfig({
80
75
  branch: (branch) =>
81
76
  branch.name === "preview"
82
77
  ? {
83
- preview: {
84
- functions: {
85
- hello: { customDomains: ["preview.example.com"] },
86
- },
78
+ functions: {
79
+ hello: { customDomains: ["preview.example.com"] },
87
80
  },
88
81
  }
89
82
  : {},
@@ -107,13 +100,11 @@ esbuild is the default. A file `source` is the entry; a directory is searched fo
107
100
 
108
101
  ```ts
109
102
  export default defineConfig({
110
- preview: {
111
- functions: {
112
- mastra: {
113
- name: "Mastra server",
114
- source: ".mastra/output",
115
- bundler: "none",
116
- },
103
+ functions: {
104
+ mastra: {
105
+ name: "Mastra server",
106
+ source: ".mastra/output",
107
+ bundler: "none",
117
108
  },
118
109
  },
119
110
  });
@@ -129,13 +120,11 @@ A function's `source` is bundled with esbuild at deploy time, and a package back
129
120
 
130
121
  ```ts
131
122
  export default defineConfig({
132
- preview: {
133
- functions: {
134
- resize: {
135
- name: "Resize",
136
- source: "./functions/resize.ts",
137
- externalPackages: ["sharp"],
138
- },
123
+ functions: {
124
+ resize: {
125
+ name: "Resize",
126
+ source: "./functions/resize.ts",
127
+ externalPackages: ["sharp"],
139
128
  },
140
129
  },
141
130
  });
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccess
2
2
  import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, isPartialBranchCreateError, isPlatformError } from "./lib/errors.js";
3
3
  import { CreateBranchInput, CreateBucketInput, CreateCredentialInput, CreateProjectInput, CreateTriggerInput, DeployFunctionInput, EnableDataApiInput, GetConnectionUriInput, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBranchStorageSnapshot, NeonBucketSnapshot, NeonCredentialMeta, NeonCredentialReveal, NeonCredentialSecret, NeonCustomDomainSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, NeonTriggerSnapshot, UpdateBranchInput, UpdateTriggerInput } from "./lib/neon-api.js";
4
4
  import { createNeonApiFromOptions } from "./lib/auth.js";
5
+ import { authoredAiGateway, authoredBuckets, authoredFunctions, deprecatedPreviewAuthoring, previewGaWarningForConfig, previewGaWarningMessage } from "./lib/authored-services.js";
5
6
  import { CredentialFeatureFlags, credentialScopesSatisfied, deriveCredentialScopes } from "./lib/credentials.js";
6
7
  import { defineConfig, resolveConfig } from "./lib/define-config.js";
7
8
  import { DiffOptions, DiffResult, PlanStep, RemotePreviewState, RemoteServiceState, RemoteState, diffConfig } from "./lib/diff.js";
@@ -10,4 +11,4 @@ import { FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, FunctionArchiveEntry
10
11
  import { LoadConfigOptions, loadConfigFromFile } from "./lib/loader.js";
11
12
  import { createRealNeonApi } from "./lib/neon-api-real.js";
12
13
  import { errors, schemas } from "./v1.js";
13
- export { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, ComputeUnit, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateCredentialInput, CreateProjectInput, CreateTriggerInput, CredentialFeatureFlags, CredentialPrincipalType, CredentialScope, DATA_API_AUTH_PROVIDERS, DataApiAuthProvider, DataApiConfig, DataApiExternalAuthConfig, DataApiInput, DataApiNeonAuthConfig, DataApiSettings, DeployFunctionInput, DiffOptions, DiffResult, DurationString, DurationUnit, EnableDataApiInput, ErrorCode, ExternalPackageDef, ExternalPackageEntry, FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, FunctionArchiveEntry, FunctionBundle, FunctionBundler, FunctionBundlerInput, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionScheduleTriggerDef, FunctionSourceEntry, FunctionTuning, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBranchStorageSnapshot, NeonBucketSnapshot, NeonCredentialMeta, NeonCredentialReveal, NeonCredentialSecret, NeonCustomDomainSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, NeonTriggerSnapshot, PartialBranchCreateError, PlanStep, PlatformError, PostgresConfig, PreviewInput, PreviewTuning, PushAbortedError, PushConflictError, PushResult, RemotePreviewState, RemoteServiceState, RemoteState, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedDataApiConfig, ResolvedExternalPackage, ResolvedFunctionConfig, ResolvedFunctionScheduleTrigger, ResolvedPreviewConfig, ServiceEnabled, ServiceToggle, ServiceToggleInput, UpdateBranchInput, UpdateTriggerInput, createNeonApiFromOptions, createRealNeonApi, credentialScopesSatisfied, defineConfig, deriveCredentialScopes, diffConfig, errors, externalPackageRoot, isFunctionArchiveEntry, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, packagesToStage, pickFunctionSourceEntry, resolveConfig, schemas };
14
+ export { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, ComputeUnit, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateCredentialInput, CreateProjectInput, CreateTriggerInput, CredentialFeatureFlags, CredentialPrincipalType, CredentialScope, DATA_API_AUTH_PROVIDERS, DataApiAuthProvider, DataApiConfig, DataApiExternalAuthConfig, DataApiInput, DataApiNeonAuthConfig, DataApiSettings, DeployFunctionInput, DiffOptions, DiffResult, DurationString, DurationUnit, EnableDataApiInput, ErrorCode, ExternalPackageDef, ExternalPackageEntry, FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, FunctionArchiveEntry, FunctionBundle, FunctionBundler, FunctionBundlerInput, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionScheduleTriggerDef, FunctionSourceEntry, FunctionTuning, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBranchStorageSnapshot, NeonBucketSnapshot, NeonCredentialMeta, NeonCredentialReveal, NeonCredentialSecret, NeonCustomDomainSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, NeonTriggerSnapshot, PartialBranchCreateError, PlanStep, PlatformError, PostgresConfig, PreviewInput, PreviewTuning, PushAbortedError, PushConflictError, PushResult, RemotePreviewState, RemoteServiceState, RemoteState, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedDataApiConfig, ResolvedExternalPackage, ResolvedFunctionConfig, ResolvedFunctionScheduleTrigger, ResolvedPreviewConfig, ServiceEnabled, ServiceToggle, ServiceToggleInput, UpdateBranchInput, UpdateTriggerInput, authoredAiGateway, authoredBuckets, authoredFunctions, createNeonApiFromOptions, createRealNeonApi, credentialScopesSatisfied, defineConfig, deprecatedPreviewAuthoring, deriveCredentialScopes, diffConfig, errors, externalPackageRoot, isFunctionArchiveEntry, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, packagesToStage, pickFunctionSourceEntry, previewGaWarningForConfig, previewGaWarningMessage, resolveConfig, schemas };
package/dist/index.js CHANGED
@@ -3,10 +3,11 @@ import { externalPackageRoot, packagesToStage } from "./lib/external-packages.js
3
3
  import { DATA_API_AUTH_PROVIDERS } from "./lib/types.js";
4
4
  import { createRealNeonApi } from "./lib/neon-api-real.js";
5
5
  import { createNeonApiFromOptions } from "./lib/auth.js";
6
+ import { authoredAiGateway, authoredBuckets, authoredFunctions, deprecatedPreviewAuthoring, previewGaWarningForConfig, previewGaWarningMessage } from "./lib/authored-services.js";
6
7
  import { credentialScopesSatisfied, deriveCredentialScopes } from "./lib/credentials.js";
7
8
  import { defineConfig, resolveConfig } from "./lib/define-config.js";
8
9
  import { diffConfig } from "./lib/diff.js";
9
10
  import { FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, isFunctionArchiveEntry, pickFunctionSourceEntry } from "./lib/function-entries.js";
10
11
  import { loadConfigFromFile } from "./lib/loader.js";
11
12
  import { errors, schemas } from "./v1.js";
12
- export { ConfigLoadError, ConfigValidationError, DATA_API_AUTH_PROVIDERS, ErrorCode, FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, createNeonApiFromOptions, createRealNeonApi, credentialScopesSatisfied, defineConfig, deriveCredentialScopes, diffConfig, errors, externalPackageRoot, isFunctionArchiveEntry, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, packagesToStage, pickFunctionSourceEntry, resolveConfig, schemas };
13
+ export { ConfigLoadError, ConfigValidationError, DATA_API_AUTH_PROVIDERS, ErrorCode, FUNCTION_ARCHIVE_ENTRIES, FUNCTION_SOURCE_ENTRIES, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, authoredAiGateway, authoredBuckets, authoredFunctions, createNeonApiFromOptions, createRealNeonApi, credentialScopesSatisfied, defineConfig, deprecatedPreviewAuthoring, deriveCredentialScopes, diffConfig, errors, externalPackageRoot, isFunctionArchiveEntry, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, packagesToStage, pickFunctionSourceEntry, previewGaWarningForConfig, previewGaWarningMessage, resolveConfig, schemas };
@@ -0,0 +1,36 @@
1
+ import { BranchTarget, BranchTuning, BucketDef, Config, FunctionDef, PreviewInput, ServiceToggleInput } from "./types.js";
2
+
3
+ //#region src/lib/authored-services.d.ts
4
+ /** The home a static service can be authored in. */
5
+ type AuthoredServiceKey = "aiGateway" | "functions" | "buckets";
6
+ declare function authoredFunctions(config: Pick<Config, "functions" | "preview">): Record<string, FunctionDef> | undefined;
7
+ declare function authoredBuckets(config: Pick<Config, "buckets" | "preview">): Record<string, BucketDef> | undefined;
8
+ declare function authoredAiGateway(config: Pick<Config, "aiGateway" | "preview">): ServiceToggleInput | undefined;
9
+ /**
10
+ * Fold top-level GA keys and deprecated `preview` keys into the shape
11
+ * {@link import("./define-config.js").resolveConfig} already consumes.
12
+ *
13
+ * Collision between the two homes is rejected at validation time, so each field
14
+ * has at most one source. An empty `preview: {}` still produces an empty object
15
+ * so resolve keeps today's `resolved.preview` of empty arrays.
16
+ */
17
+ declare function mergeAuthoredPreview(config: Config): PreviewInput | undefined;
18
+ /** Per-function branch tuning from top-level `functions` or deprecated `preview.functions`. */
19
+ declare function authoredFunctionTuning(tuning: BranchTuning): NonNullable<BranchTuning["functions"]>;
20
+ /**
21
+ * Deprecated authoring paths still present on the policy. Used by the CLI deploy/apply
22
+ * warning. Empty `preview: {}` contributes nothing.
23
+ */
24
+ declare function deprecatedPreviewAuthoring(config: Pick<Config, "preview">, tuning?: Pick<BranchTuning, "preview" | "functions">): string[];
25
+ /** One-line deploy warning naming each deprecated path and its GA replacement. */
26
+ declare function previewGaWarningMessage(keys: readonly string[]): string;
27
+ /**
28
+ * Deprecation warning for a policy that still authors GA services under `preview`.
29
+ * Pass the same {@link BranchTarget} `resolveConfig` / apply will use so
30
+ * `branch.preview.functions` tuning on that branch is included. Without a
31
+ * target, only static `preview.*` keys are reported.
32
+ */
33
+ declare function previewGaWarningForConfig(config: Config, target?: BranchTarget): string | null;
34
+ //#endregion
35
+ export { AuthoredServiceKey, authoredAiGateway, authoredBuckets, authoredFunctionTuning, authoredFunctions, deprecatedPreviewAuthoring, mergeAuthoredPreview, previewGaWarningForConfig, previewGaWarningMessage };
36
+ //# sourceMappingURL=authored-services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authored-services.d.ts","names":[],"sources":["../../src/lib/authored-services.ts"],"mappings":";;;;KAYY,kBAAA;AAAA,iBAcI,iBAAA,CAdc,MAAA,EAerB,IAfqB,CAehB,MAfgB,EAAA,WAAA,GAAA,SAAA,CAAA,CAAA,EAgB3B,MAhB2B,CAAA,MAAA,EAgBZ,WAhBY,CAAA,GAAA,SAAA;AAcd,iBAMA,eAAA,CANiB,MAAA,EAOxB,IAPwB,CAOnB,MAPmB,EAAA,SAAA,GAAA,SAAA,CAAA,CAAA,EAQ9B,MAR8B,CAAA,MAAA,EAQf,SARe,CAAA,GAAA,SAAA;AAAA,iBAYjB,iBAAA,CAZiB,MAAA,EAaxB,IAbwB,CAanB,MAbmB,EAAA,WAAA,GAAA,SAAA,CAAA,CAAA,EAc9B,kBAd8B,GAAA,SAAA;AACnB;AAAL;AACS;AAAf;AAAM;AAIT;AAA+B;AACjB;AAAL,iBAmBO,oBAAA,CAnBP,MAAA,EAmBoC,MAnBpC,CAAA,EAmB6C,YAnB7C,GAAA,SAAA;AACS;AAAf,iBAsCa,sBAAA,CAtCb,MAAA,EAuCM,YAvCN,CAAA,EAwCA,WAxCA,CAwCY,YAxCZ,CAAA,WAAA,CAAA,CAAA;AAAM;AAIT;AAAiC;AACnB;AAAL,iBA2CO,0BAAA,CA3CP,MAAA,EA4CA,IA5CA,CA4CK,MA5CL,EAAA,SAAA,CAAA,EAAA,MAAA,CAAA,EA6CC,IA7CD,CA6CM,YA7CN,EAAA,SAAA,GAAA,WAAA,CAAA,CAAA,EAAA,MAAA,EAAA;AACN;AAAkB,iBAyDL,uBAAA,CAzDK,IAAA,EAAA,SAAA,MAAA,EAAA,CAAA,EAAA,MAAA;AAYrB;AAAoC;AAAS;AAAS;AAAY;AAoBlE;AAAsC,iBAiDtB,yBAAA,CAjDsB,MAAA,EAkD7B,MAlD6B,EAAA,MAAA,CAAA,EAmD5B,YAnD4B,CAAA,EAAA,MAAA,GAAA,IAAA"}
@@ -0,0 +1,89 @@
1
+ import { branchTuningSchema } from "./schema.js";
2
+ //#region src/lib/authored-services.ts
3
+ const PREVIEW_PATH = {
4
+ aiGateway: "preview.aiGateway",
5
+ functions: "preview.functions",
6
+ buckets: "preview.buckets"
7
+ };
8
+ const GA_PATH = {
9
+ aiGateway: "aiGateway",
10
+ functions: "functions",
11
+ buckets: "buckets"
12
+ };
13
+ function authoredFunctions(config) {
14
+ return config.functions ?? config.preview?.functions;
15
+ }
16
+ function authoredBuckets(config) {
17
+ return config.buckets ?? config.preview?.buckets;
18
+ }
19
+ function authoredAiGateway(config) {
20
+ return config.aiGateway ?? config.preview?.aiGateway;
21
+ }
22
+ /**
23
+ * Fold top-level GA keys and deprecated `preview` keys into the shape
24
+ * {@link import("./define-config.js").resolveConfig} already consumes.
25
+ *
26
+ * Collision between the two homes is rejected at validation time, so each field
27
+ * has at most one source. An empty `preview: {}` still produces an empty object
28
+ * so resolve keeps today's `resolved.preview` of empty arrays.
29
+ */
30
+ function mergeAuthoredPreview(config) {
31
+ const functions = authoredFunctions(config);
32
+ const buckets = authoredBuckets(config);
33
+ const aiGateway = authoredAiGateway(config);
34
+ if (functions === void 0 && buckets === void 0 && aiGateway === void 0 && config.preview === void 0) return;
35
+ const merged = {};
36
+ if (aiGateway !== void 0) merged.aiGateway = aiGateway;
37
+ if (functions !== void 0) merged.functions = functions;
38
+ if (buckets !== void 0) merged.buckets = buckets;
39
+ return merged;
40
+ }
41
+ /** Per-function branch tuning from top-level `functions` or deprecated `preview.functions`. */
42
+ function authoredFunctionTuning(tuning) {
43
+ return tuning.functions ?? tuning.preview?.functions ?? {};
44
+ }
45
+ /**
46
+ * Deprecated authoring paths still present on the policy. Used by the CLI deploy/apply
47
+ * warning. Empty `preview: {}` contributes nothing.
48
+ */
49
+ function deprecatedPreviewAuthoring(config, tuning) {
50
+ const keys = [];
51
+ for (const key of [
52
+ "aiGateway",
53
+ "functions",
54
+ "buckets"
55
+ ]) if (config.preview?.[key] !== void 0) keys.push(PREVIEW_PATH[key]);
56
+ if (tuning?.preview?.functions !== void 0) keys.push("branch.preview.functions");
57
+ return keys;
58
+ }
59
+ /** One-line deploy warning naming each deprecated path and its GA replacement. */
60
+ function previewGaWarningMessage(keys) {
61
+ return `These neon.ts keys are now GA and can be lifted out of preview: ${keys.map((key) => {
62
+ switch (key) {
63
+ case "preview.aiGateway": return `${PREVIEW_PATH.aiGateway} → ${GA_PATH.aiGateway}`;
64
+ case "preview.functions": return `${PREVIEW_PATH.functions} → ${GA_PATH.functions}`;
65
+ case "preview.buckets": return `${PREVIEW_PATH.buckets} → ${GA_PATH.buckets}`;
66
+ case "branch.preview.functions": return "branch.preview.functions → branch.functions";
67
+ default: return key;
68
+ }
69
+ }).join(", ")}.`;
70
+ }
71
+ /**
72
+ * Deprecation warning for a policy that still authors GA services under `preview`.
73
+ * Pass the same {@link BranchTarget} `resolveConfig` / apply will use so
74
+ * `branch.preview.functions` tuning on that branch is included. Without a
75
+ * target, only static `preview.*` keys are reported.
76
+ */
77
+ function previewGaWarningForConfig(config, target) {
78
+ const keys = new Set(deprecatedPreviewAuthoring(config));
79
+ if (typeof config.branch === "function" && target !== void 0) try {
80
+ const parsed = branchTuningSchema.safeParse(config.branch(target) ?? {});
81
+ if (parsed.success) for (const key of deprecatedPreviewAuthoring(config, parsed.data)) keys.add(key);
82
+ } catch {}
83
+ if (keys.size === 0) return null;
84
+ return previewGaWarningMessage([...keys]);
85
+ }
86
+ //#endregion
87
+ export { authoredAiGateway, authoredBuckets, authoredFunctionTuning, authoredFunctions, deprecatedPreviewAuthoring, mergeAuthoredPreview, previewGaWarningForConfig, previewGaWarningMessage };
88
+
89
+ //# sourceMappingURL=authored-services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authored-services.js","names":[],"sources":["../../src/lib/authored-services.ts"],"sourcesContent":["import { branchTuningSchema } from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tBranchTuning,\n\tBucketDef,\n\tConfig,\n\tFunctionDef,\n\tPreviewInput,\n\tServiceToggleInput,\n} from \"./types.js\";\n\n/** The home a static service can be authored in. */\nexport type AuthoredServiceKey = \"aiGateway\" | \"functions\" | \"buckets\";\n\nconst PREVIEW_PATH: Record<AuthoredServiceKey, string> = {\n\taiGateway: \"preview.aiGateway\",\n\tfunctions: \"preview.functions\",\n\tbuckets: \"preview.buckets\",\n};\n\nconst GA_PATH: Record<AuthoredServiceKey, string> = {\n\taiGateway: \"aiGateway\",\n\tfunctions: \"functions\",\n\tbuckets: \"buckets\",\n};\n\nexport function authoredFunctions(\n\tconfig: Pick<Config, \"functions\" | \"preview\">,\n): Record<string, FunctionDef> | undefined {\n\treturn config.functions ?? config.preview?.functions;\n}\n\nexport function authoredBuckets(\n\tconfig: Pick<Config, \"buckets\" | \"preview\">,\n): Record<string, BucketDef> | undefined {\n\treturn config.buckets ?? config.preview?.buckets;\n}\n\nexport function authoredAiGateway(\n\tconfig: Pick<Config, \"aiGateway\" | \"preview\">,\n): ServiceToggleInput | undefined {\n\treturn config.aiGateway ?? config.preview?.aiGateway;\n}\n\n/**\n * Fold top-level GA keys and deprecated `preview` keys into the shape\n * {@link import(\"./define-config.js\").resolveConfig} already consumes.\n *\n * Collision between the two homes is rejected at validation time, so each field\n * has at most one source. An empty `preview: {}` still produces an empty object\n * so resolve keeps today's `resolved.preview` of empty arrays.\n */\nexport function mergeAuthoredPreview(config: Config): PreviewInput | undefined {\n\tconst functions = authoredFunctions(config);\n\tconst buckets = authoredBuckets(config);\n\tconst aiGateway = authoredAiGateway(config);\n\tif (\n\t\tfunctions === undefined &&\n\t\tbuckets === undefined &&\n\t\taiGateway === undefined &&\n\t\tconfig.preview === undefined\n\t) {\n\t\treturn undefined;\n\t}\n\tconst merged: PreviewInput = {};\n\tif (aiGateway !== undefined) merged.aiGateway = aiGateway;\n\tif (functions !== undefined) merged.functions = functions;\n\tif (buckets !== undefined) merged.buckets = buckets;\n\treturn merged;\n}\n\n/** Per-function branch tuning from top-level `functions` or deprecated `preview.functions`. */\nexport function authoredFunctionTuning(\n\ttuning: BranchTuning,\n): NonNullable<BranchTuning[\"functions\"]> {\n\treturn tuning.functions ?? tuning.preview?.functions ?? {};\n}\n\n/**\n * Deprecated authoring paths still present on the policy. Used by the CLI deploy/apply\n * warning. Empty `preview: {}` contributes nothing.\n */\nexport function deprecatedPreviewAuthoring(\n\tconfig: Pick<Config, \"preview\">,\n\ttuning?: Pick<BranchTuning, \"preview\" | \"functions\">,\n): string[] {\n\tconst keys: string[] = [];\n\tfor (const key of [\"aiGateway\", \"functions\", \"buckets\"] as const) {\n\t\tif (config.preview?.[key] !== undefined) keys.push(PREVIEW_PATH[key]);\n\t}\n\tif (tuning?.preview?.functions !== undefined) {\n\t\tkeys.push(\"branch.preview.functions\");\n\t}\n\treturn keys;\n}\n\n/** One-line deploy warning naming each deprecated path and its GA replacement. */\nexport function previewGaWarningMessage(keys: readonly string[]): string {\n\tconst lifts = keys.map((key) => {\n\t\tswitch (key) {\n\t\t\tcase \"preview.aiGateway\":\n\t\t\t\treturn `${PREVIEW_PATH.aiGateway} → ${GA_PATH.aiGateway}`;\n\t\t\tcase \"preview.functions\":\n\t\t\t\treturn `${PREVIEW_PATH.functions} → ${GA_PATH.functions}`;\n\t\t\tcase \"preview.buckets\":\n\t\t\t\treturn `${PREVIEW_PATH.buckets} → ${GA_PATH.buckets}`;\n\t\t\tcase \"branch.preview.functions\":\n\t\t\t\treturn \"branch.preview.functions → branch.functions\";\n\t\t\tdefault:\n\t\t\t\treturn key;\n\t\t}\n\t});\n\treturn `These neon.ts keys are now GA and can be lifted out of preview: ${lifts.join(\", \")}.`;\n}\n\n/**\n * Deprecation warning for a policy that still authors GA services under `preview`.\n * Pass the same {@link BranchTarget} `resolveConfig` / apply will use so\n * `branch.preview.functions` tuning on that branch is included. Without a\n * target, only static `preview.*` keys are reported.\n */\nexport function previewGaWarningForConfig(\n\tconfig: Config,\n\ttarget?: BranchTarget,\n): string | null {\n\tconst keys = new Set(deprecatedPreviewAuthoring(config));\n\tif (typeof config.branch === \"function\" && target !== undefined) {\n\t\ttry {\n\t\t\tconst parsed = branchTuningSchema.safeParse(\n\t\t\t\tconfig.branch(target) ?? {},\n\t\t\t);\n\t\t\tif (parsed.success) {\n\t\t\t\tfor (const key of deprecatedPreviewAuthoring(\n\t\t\t\t\tconfig,\n\t\t\t\t\tparsed.data,\n\t\t\t\t)) {\n\t\t\t\t\tkeys.add(key);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// A throwing closure is a resolveConfig error, not a warning path.\n\t\t}\n\t}\n\tif (keys.size === 0) return null;\n\treturn previewGaWarningMessage([...keys]);\n}\n"],"mappings":";;AAcA,MAAM,eAAmD;CACxD,WAAW;CACX,WAAW;CACX,SAAS;AACV;AAEA,MAAM,UAA8C;CACnD,WAAW;CACX,WAAW;CACX,SAAS;AACV;AAEA,SAAgB,kBACf,QAC0C;CAC1C,OAAO,OAAO,aAAa,OAAO,SAAS;AAC5C;AAEA,SAAgB,gBACf,QACwC;CACxC,OAAO,OAAO,WAAW,OAAO,SAAS;AAC1C;AAEA,SAAgB,kBACf,QACiC;CACjC,OAAO,OAAO,aAAa,OAAO,SAAS;AAC5C;;;;;;;;;AAUA,SAAgB,qBAAqB,QAA0C;CAC9E,MAAM,YAAY,kBAAkB,MAAM;CAC1C,MAAM,UAAU,gBAAgB,MAAM;CACtC,MAAM,YAAY,kBAAkB,MAAM;CAC1C,IACC,cAAc,KAAA,KACd,YAAY,KAAA,KACZ,cAAc,KAAA,KACd,OAAO,YAAY,KAAA,GAEnB;CAED,MAAM,SAAuB,CAAC;CAC9B,IAAI,cAAc,KAAA,GAAW,OAAO,YAAY;CAChD,IAAI,cAAc,KAAA,GAAW,OAAO,YAAY;CAChD,IAAI,YAAY,KAAA,GAAW,OAAO,UAAU;CAC5C,OAAO;AACR;;AAGA,SAAgB,uBACf,QACyC;CACzC,OAAO,OAAO,aAAa,OAAO,SAAS,aAAa,CAAC;AAC1D;;;;;AAMA,SAAgB,2BACf,QACA,QACW;CACX,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,OAAO;EAAC;EAAa;EAAa;CAAS,GACrD,IAAI,OAAO,UAAU,SAAS,KAAA,GAAW,KAAK,KAAK,aAAa,IAAI;CAErE,IAAI,QAAQ,SAAS,cAAc,KAAA,GAClC,KAAK,KAAK,0BAA0B;CAErC,OAAO;AACR;;AAGA,SAAgB,wBAAwB,MAAiC;CAexE,OAAO,mEAdO,KAAK,KAAK,QAAQ;EAC/B,QAAQ,KAAR;GACC,KAAK,qBACJ,OAAO,GAAG,aAAa,UAAU,KAAK,QAAQ;GAC/C,KAAK,qBACJ,OAAO,GAAG,aAAa,UAAU,KAAK,QAAQ;GAC/C,KAAK,mBACJ,OAAO,GAAG,aAAa,QAAQ,KAAK,QAAQ;GAC7C,KAAK,4BACJ,OAAO;GACR,SACC,OAAO;EACT;CACD,CAC8E,CAAC,CAAC,KAAK,IAAI,EAAE;AAC5F;;;;;;;AAQA,SAAgB,0BACf,QACA,QACgB;CAChB,MAAM,OAAO,IAAI,IAAI,2BAA2B,MAAM,CAAC;CACvD,IAAI,OAAO,OAAO,WAAW,cAAc,WAAW,KAAA,GACrD,IAAI;EACH,MAAM,SAAS,mBAAmB,UACjC,OAAO,OAAO,MAAM,KAAK,CAAC,CAC3B;EACA,IAAI,OAAO,SACV,KAAK,MAAM,OAAO,2BACjB,QACA,OAAO,IACR,GACC,KAAK,IAAI,GAAG;CAGf,QAAQ,CAER;CAED,IAAI,KAAK,SAAS,GAAG,OAAO;CAC5B,OAAO,wBAAwB,CAAC,GAAG,IAAI,CAAC;AACzC"}
@@ -65,6 +65,8 @@ type PreviewAutocomplete<Preview> = (Preview extends {
65
65
  } ? {
66
66
  buckets: { [Name in keyof B]: BucketDef };
67
67
  } : unknown);
68
+ type FunctionsAutocomplete<Functions> = Functions extends Record<string, unknown> ? { [Slug in keyof Functions]: FunctionDef } : unknown;
69
+ type BucketsAutocomplete<Buckets> = Buckets extends Record<string, unknown> ? { [Name in keyof Buckets]: BucketDef } : unknown;
68
70
  /**
69
71
  * Validate and freeze a Neon branch policy.
70
72
  *
@@ -74,20 +76,18 @@ type PreviewAutocomplete<Preview> = (Preview extends {
74
76
  *
75
77
  * export default defineConfig({
76
78
  * auth: true,
77
- * preview: {
78
- * functions: {
79
- * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
80
- * },
79
+ * functions: {
80
+ * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
81
81
  * },
82
82
  * branch: (branch) => ({ protected: branch.name === "main" }),
83
83
  * });
84
84
  * ```
85
85
  *
86
- * The policy is split into a **static** existential set (top-level `auth` / `dataApi`
87
- * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
88
- * static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`
89
- * are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function
90
- * deploy settings), never change what exists.
86
+ * The policy is split into a **static** existential set (top-level `auth` / `dataApi` /
87
+ * `aiGateway` / `functions` / `buckets`, plus the deprecated `preview` aliases) and a
88
+ * **dynamic** per-branch `branch` closure. The static half determines which secrets exist —
89
+ * so `NeonEnv<typeof config>` and `parseEnv` are exact — while the closure can only *tune*
90
+ * a branch (lifecycle, compute, per-function deploy settings), never change what exists.
91
91
  *
92
92
  * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
93
93
  * being decided for (not a live handle); switch on its facts (`branch.name`,
@@ -98,16 +98,20 @@ type PreviewAutocomplete<Preview> = (Preview extends {
98
98
  * Pure: no I/O, no side effects. The static parts are validated here; the closure's output
99
99
  * is validated every time it is evaluated so errors point at the concrete branch target.
100
100
  */
101
- declare function defineConfig<const Auth extends ServiceToggleInput | undefined = undefined, const DataApi extends DataApiInput | undefined = undefined, const Preview extends PreviewInput | undefined = undefined>(input: {
101
+ declare function defineConfig<const Auth extends ServiceToggleInput | undefined = undefined, const DataApi extends DataApiInput | undefined = undefined, const Preview extends PreviewInput | undefined = undefined, const Functions extends Record<string, FunctionDef> | undefined = undefined, const Buckets extends Record<string, BucketDef> | undefined = undefined, const AiGateway extends ServiceToggleInput | undefined = undefined>(input: {
102
102
  auth?: Auth & ServiceToggleInput;
103
103
  dataApi?: DataApiField<Auth, DataApi>;
104
+ aiGateway?: AiGateway & ServiceToggleInput;
105
+ functions?: Functions & Record<string, FunctionDef> & FunctionsAutocomplete<Functions>;
106
+ buckets?: Buckets & Record<string, BucketDef> & BucketsAutocomplete<Buckets>;
107
+ /** @deprecated Use top-level `aiGateway`, `functions`, and `buckets`. */
104
108
  preview?: Preview & PreviewInput & PreviewAutocomplete<Preview>;
105
- branch?: BranchTuningFn<Preview>;
106
- }): Config<Auth, DataApi, Preview>;
109
+ branch?: BranchTuningFn<Preview, Functions>;
110
+ }): Config<Auth, DataApi, Preview, Functions, Buckets, AiGateway>;
107
111
  /**
108
112
  * Evaluate a branch policy for a specific branch target and return a normalized config.
109
113
  *
110
- * Merges the static existential set (services + preview functions/buckets) with the
114
+ * Merges the static existential set (services + functions/buckets) with the
111
115
  * per-branch tuning returned by the `branch` closure into the same {@link
112
116
  * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
113
117
  */
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;AA0BoB;AAcI;AACR;AAAf,KADI,mBACJ,CAAA,OAAA,CAAA,GAAA,cAAA,CAAe,OAAf,CAAA,SAAA,IAAA,GAAA,CACI,OADJ,CAAA,SAAA,CAAA;EACI,YAAA,EAAA,UAAA;AAAO,CAAA,CAAA,GAAA,KAAA,GAAA,IAAA,GAAA,KAAA;AAiBZ;AAqBA;AAAwB;AACH;AAApB;AACkB;AAAf;AACC;AAAU;AACV;AACD,KA1BQ,oBAAA,GA0BR,+QAAA;AAAU;AAAY;AAAC;AAoBH;AAAa;AACJ;AAAI;AAEnC;AAC8B;AAAI;AAAS;AAoC7C;AAA4B;AACR;AACG;AACA;AAQf,KA5EI,YA4EJ,CAAA,IAAA,EAAA,OAAA,CAAA,GA3EP,mBA2EO,CA3Ea,OA2Eb,CAAA,SAAA,IAAA,GA1EJ,cA0EI,CA1EW,IA0EX,CAAA,SAAA,IAAA,GAzEH,OAyEG,GAzEO,YAyEP,GAxEH,oBAwEG,GAvEJ,OAuEI,GAvEM,YAuEN;AAAO;AAIS;AAAM;AAAnB;AAIA;AAAU;AAAmC;AAApB;AACX;AAAf;AACC;AAAM;AAAS;AAAtB;AAAM;AA4BV;AAA6B;AACpB;AACA,KA3FJ,mBA2FI,CAAA,OAAA,CAAA,GAAA,CA3F4B,OA2F5B,SAAA;EACN,SAAA,EAAA,KAAA,EAAA;AAAoB,CAAA,GAAA;EAiOP,SAAA,EAAA,iBA5TiB,IAAI;gBAEnC;;;4BAC8B,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCpB,gCACI,kEACG,4DACA;SAQf,OAAO;YAIJ,aAAa,MAAM;YAInB,UAAU,eAAe,oBAAoB;WAC9C,eAAe;IACrB,OAAO,MAAM,SAAS;;;;;;;;iBA4BV,aAAA,SACP,gBACA,eACN;;;;;;iBAiOa,eAAA"}
1
+ {"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;AA8BoB;AAcI;AACR;AAAf,KADI,mBACJ,CAAA,OAAA,CAAA,GAAA,cAAA,CAAe,OAAf,CAAA,SAAA,IAAA,GAAA,CACI,OADJ,CAAA,SAAA,CAAA;EACI,YAAA,EAAA,UAAA;AAAO,CAAA,CAAA,GAAA,KAAA,GAAA,IAAA,GAAA,KAAA;AAiBZ;AAqBA;AAAwB;AACH;AAApB;AACkB;AAAf;AACC;AAAU;AACV;AACD,KA1BQ,oBAAA,GA0BR,+QAAA;AAAU;AAAY;AAAC;AAoBH;AAAa;AACJ;AAAI;AAEnC;AAC8B;AAAI;AAAS;AAAA;AAGnB;AACzB;AAAkB;AACE;AAAY,KAlCrB,YAkCqB,CAAA,IAAA,EAAA,OAAA,CAAA,GAjChC,mBAiCgC,CAjCZ,OAiCY,CAAA,SAAA,IAAA,GAhC7B,cAgC6B,CAhCd,IAgCc,CAAA,SAAA,IAAA,GA/B5B,OA+B4B,GA/BlB,YA+BkB,GA9B5B,oBA8B4B,GA7B7B,OA6B6B,GA7BnB,YA6BmB;AAAW;AAAA;AAGpB;AACvB;AAAgB;AACI;AAAU;AAAS;AAkCxC;AAA4B;AACR;AACG;AACA;AACiB;AAAf;AACa;AAAf;AACE;AAQjB,KA9DH,mBA8DG,CAAA,OAAA,CAAA,GAAA,CA9D6B,OA8D7B,SAAA;EAAO,SAAA,EAAA,KAAA,EAAA;AAIS,CAAA,GAAA;EAAM,SAAA,EAAA,WAAnB,MAjEsB,CAiEtB,GAjE0B,WAiE1B,EACE;AAAY,CAAA,GAAA,OAAA,CAAA,GAAA,CAhEvB,OAgEuB,SAAA;EACZ,OAAA,EAAA,KAAA,EAAA;AACI,CAAA,GAAA;EAAf,OAAA,EAAA,WACsB,MAlEQ,CAkER,GAlEY,SAkEZ,EAAtB;AACS,CAAA,GAAA,OAAA,CAAA;AACM,KAjEZ,qBAiEY,CAAA,SAAA,CAAA,GAhEhB,SAgEgB,SAhEE,MAgEF,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,WAAf,MA/DmB,SA+DnB,GA/D+B,WA+D/B,EACoB,GAAA,OAAA;AAApB,KA7DG,mBA6DH,CAAA,OAAA,CAAA,GA5DD,OA4DC,SA5De,MA4Df,CAAA,MAAA,EAAA,OAAA,CAAA,GAAA,WAKS,MAhEU,OAgEV,GAhEoB,SAgEpB,EAAU,GAAA,OAAA;AAAmC;AAApB;AACX;AAAS;AAAxB;AACC;AAAM;AAAS;AAAS;AAAW;AAAS;AAAnD;AAAM;AAmCV;AAA6B;AACpB;AACA;AACN;AAAoB;AAqOvB;;;;;;;;;;;;iBA3SgB,gCACI,kEACG,4DACA,8DACE,eAAe,4DACjB,eAAe,4DACb;SAQjB,OAAO;YAIJ,aAAa,MAAM;cACjB,YAAY;cACZ,YACX,eAAe,eACf,sBAAsB;YACb,UACT,eAAe,aACf,oBAAoB;;YAKX,UAAU,eAAe,oBAAoB;WAC9C,eAAe,SAAS;IAC9B,OAAO,MAAM,SAAS,SAAS,WAAW,SAAS;;;;;;;;iBAmCvC,aAAA,SACP,gBACA,eACN;;;;;;iBAqOa,eAAA"}
@@ -3,6 +3,7 @@ import { normalizeCustomDomain } from "./custom-domain.js";
3
3
  import { parseBranchTtl } from "./duration.js";
4
4
  import { normalizeExternalPackage } from "./external-packages.js";
5
5
  import { branchTuningSchema, configInputSchema, formatZodIssues } from "./schema.js";
6
+ import { authoredFunctionTuning, mergeAuthoredPreview } from "./authored-services.js";
6
7
  //#region src/lib/define-config.ts
7
8
  /** Default deploy parameters applied to functions that omit them in `neon.ts`. */
8
9
  const DEFAULT_FUNCTION_RUNTIME = "nodejs24";
@@ -17,20 +18,18 @@ const REGION_PREFIX = /^(aws|azure|gcp)-/;
17
18
  *
18
19
  * export default defineConfig({
19
20
  * auth: true,
20
- * preview: {
21
- * functions: {
22
- * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
23
- * },
21
+ * functions: {
22
+ * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
24
23
  * },
25
24
  * branch: (branch) => ({ protected: branch.name === "main" }),
26
25
  * });
27
26
  * ```
28
27
  *
29
- * The policy is split into a **static** existential set (top-level `auth` / `dataApi`
30
- * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
31
- * static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`
32
- * are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function
33
- * deploy settings), never change what exists.
28
+ * The policy is split into a **static** existential set (top-level `auth` / `dataApi` /
29
+ * `aiGateway` / `functions` / `buckets`, plus the deprecated `preview` aliases) and a
30
+ * **dynamic** per-branch `branch` closure. The static half determines which secrets exist —
31
+ * so `NeonEnv<typeof config>` and `parseEnv` are exact — while the closure can only *tune*
32
+ * a branch (lifecycle, compute, per-function deploy settings), never change what exists.
34
33
  *
35
34
  * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
36
35
  * being decided for (not a live handle); switch on its facts (`branch.name`,
@@ -42,7 +41,7 @@ const REGION_PREFIX = /^(aws|azure|gcp)-/;
42
41
  * is validated every time it is evaluated so errors point at the concrete branch target.
43
42
  */
44
43
  function defineConfig(input) {
45
- if (typeof input === "function") throw new ConfigValidationError(["defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, preview: { … }, branch: (branch) => ({ … }) })`.", "The static services/preview set moved to the top level; per-branch logic moved into the `branch` closure."]);
44
+ if (typeof input === "function") throw new ConfigValidationError(["defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, functions: { … }, branch: (branch) => ({ … }) })`.", "The static services set moved to the top level; per-branch logic moved into the `branch` closure."]);
46
45
  if (input === null || typeof input !== "object") throw new ConfigValidationError(["defineConfig expects a configuration object: `export default defineConfig({ … })`."]);
47
46
  const parsed = configInputSchema.safeParse(input);
48
47
  if (!parsed.success) throw new ConfigValidationError(formatZodIssues(parsed.error));
@@ -51,7 +50,7 @@ function defineConfig(input) {
51
50
  /**
52
51
  * Evaluate a branch policy for a specific branch target and return a normalized config.
53
52
  *
54
- * Merges the static existential set (services + preview functions/buckets) with the
53
+ * Merges the static existential set (services + functions/buckets) with the
55
54
  * per-branch tuning returned by the `branch` closure into the same {@link
56
55
  * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
57
56
  */
@@ -71,7 +70,7 @@ function resolveConfig(config, branch) {
71
70
  }
72
71
  if (tuning.protected !== void 0) resolved.protected = tuning.protected;
73
72
  if (tuning.postgres?.computeSettings) resolved.postgres = { computeSettings: { ...tuning.postgres.computeSettings } };
74
- const preview = resolvePreviewConfig(config.preview, tuning, branch);
73
+ const preview = resolvePreviewConfig(mergeAuthoredPreview(config), tuning, branch);
75
74
  if (preview) resolved.preview = preview;
76
75
  return resolved;
77
76
  }
@@ -137,7 +136,7 @@ function normalizeDataApiSettings(settings) {
137
136
  */
138
137
  function resolvePreviewConfig(preview, tuning, branch) {
139
138
  if (!preview) return void 0;
140
- const fnTuning = tuning.preview?.functions ?? {};
139
+ const fnTuning = authoredFunctionTuning(tuning);
141
140
  const functions = Object.entries(preview.functions ?? {}).map(([slug, def]) => resolveFunctionConfig(slug, def, fnTuning[slug] ?? {}, branch));
142
141
  assertUniqueResolvedCustomDomains(functions);
143
142
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import { normalizeCustomDomain } from \"./custom-domain.js\";\nimport { parseBranchTtl } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport { normalizeExternalPackage } from \"./external-packages.js\";\nimport {\n\tbranchTuningSchema,\n\tconfigInputSchema,\n\tformatZodIssues,\n} from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tBranchTuning,\n\tBranchTuningFn,\n\tBucketDef,\n\tConfig,\n\tDataApiInput,\n\tDataApiSettings,\n\tFunctionDef,\n\tFunctionTuning,\n\tPreviewInput,\n\tResolvedBranchConfig,\n\tResolvedDataApiConfig,\n\tResolvedFunctionConfig,\n\tResolvedPreviewConfig,\n\tServiceEnabled,\n\tServiceToggleInput,\n} from \"./types.js\";\n\n/** Default deploy parameters applied to functions that omit them in `neon.ts`. */\nconst DEFAULT_FUNCTION_RUNTIME = \"nodejs24\" as const;\n\nconst DEFAULT_FUNCTION_BUNDLER = \"esbuild\" as const;\n\nconst REGION_PREFIX = /^(aws|azure|gcp)-/;\n\n/**\n * Whether a `dataApi` toggle is **enabled and verified by Neon Auth** at the type level: it is\n * on (see {@link ServiceEnabled}) and not the explicit `authProvider: \"external\"` variant\n * (so the default / `\"neon\"` provider). This is the case that requires top-level Neon Auth.\n */\ntype DataApiUsesNeonAuth<DataApi> =\n\tServiceEnabled<DataApi> extends true\n\t\t? [DataApi] extends [{ authProvider: \"external\" }]\n\t\t\t? false\n\t\t\t: true\n\t\t: false;\n\n/**\n * Human-readable hint surfaced as the **expected type** of `dataApi` when a Neon-Auth Data\n * API is declared without Neon Auth enabled (see {@link DataApiField}). TypeScript prints the\n * offending value against this string literal — `Type 'true' is not assignable to type\n * '…requires `auth: true`…'` — which points straight at the fix, instead of the opaque\n * `Type 'true' is not assignable to type 'never'` an intersection guard produces.\n *\n * It documents **both** fixes: enabling Neon Auth (`auth: true`), and running the Data API\n * *without* Neon Auth by verifying a third-party IdP (`authProvider: 'external'` + `jwksUrl`).\n */\n// Exported (type-only) for the type tests in `define-config.test-d.ts`; intentionally not\n// re-exported from `v1.ts` / `index.ts`, so it stays an internal implementation detail.\nexport type NeonAuthRequiredHint =\n\t\"`dataApi` with Neon Auth (the default `authProvider: 'neon'`) requires Neon Auth, so add `auth: true`. To enable the Data API WITHOUT Neon Auth, verify a third-party IdP instead: `dataApi: { authProvider: 'external', jwksUrl: 'https://your-idp/.well-known/jwks.json' }`\";\n\n/**\n * Static cross-field guard for {@link defineConfig}, expressed as the **type of the `dataApi`\n * field** rather than an intersected requirement on `auth`.\n *\n * - A Neon-Auth Data API (`authProvider: \"neon\"`, the default) with top-level `auth` enabled,\n * or any external Data API: the field keeps its normal `DataApi & DataApiInput` type (the\n * `& DataApiInput` preserves member autocomplete; the `const DataApi` still types the\n * returned {@link Config}).\n * - A Neon-Auth Data API **without** `auth` enabled: the field's expected type collapses to\n * the {@link NeonAuthRequiredHint} message, so the author sees the rule (and the two fixes)\n * right on the `dataApi` value.\n *\n * The runtime `superRefine` in {@link configInputSchema} enforces the same invariant for\n * non-typed (plain-JS) callers, so the behavior is identical — only the type-level message\n * changes.\n */\n// Exported (type-only) for the type tests in `define-config.test-d.ts`; intentionally not\n// re-exported from `v1.ts` / `index.ts`, so it stays an internal implementation detail.\nexport type DataApiField<Auth, DataApi> =\n\tDataApiUsesNeonAuth<DataApi> extends true\n\t\t? ServiceEnabled<Auth> extends true\n\t\t\t? DataApi & DataApiInput\n\t\t\t: NeonAuthRequiredHint\n\t\t: DataApi & DataApiInput;\n\n/**\n * Autocomplete bridge for the nested `preview.functions` / `preview.buckets` slug objects.\n *\n * {@link PreviewInput} types those records with a string index signature\n * (`Record<string, FunctionDef>` / `Record<string, BucketDef>`). When `defineConfig` infers\n * `const Preview`, every authored slug becomes a **named** property on the inferred literal\n * (e.g. `{ hello: { name; source } }`), and a named property **shadows** the index signature\n * when the editor computes the contextual type of that slug's value — so the rest of\n * {@link FunctionDef} / {@link BucketDef} (`env`, `dev`, `access`, …) never surfaces as\n * completions inside `hello: { … }` / `uploads: { … }`.\n *\n * Re-declaring each inferred slug's value as `FunctionDef` / `BucketDef` (a *named* member, via\n * a mapped type over the already-inferred keys) puts those members back onto the contextual\n * type without going through an index signature, which restores autocomplete. Intersected with\n * `Preview & PreviewInput` it neither widens what is accepted (the values were already\n * `FunctionDef` / `BucketDef`) nor perturbs the inferred `const Preview` — so slug inference for\n * `BranchTuningFn<Preview>` and the returned {@link Config} is unchanged.\n */\ntype PreviewAutocomplete<Preview> = (Preview extends { functions: infer F }\n\t? { functions: { [Slug in keyof F]: FunctionDef } }\n\t: unknown) &\n\t(Preview extends { buckets: infer B }\n\t\t? { buckets: { [Name in keyof B]: BucketDef } }\n\t\t: unknown);\n\n/**\n * Validate and freeze a Neon branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neon/config/v1\";\n *\n * export default defineConfig({\n * auth: true,\n * preview: {\n * functions: {\n * hello: { name: \"Hello\", source: \"./functions/hello.ts\", dev: { port: 8787 } },\n * },\n * },\n * branch: (branch) => ({ protected: branch.name === \"main\" }),\n * });\n * ```\n *\n * The policy is split into a **static** existential set (top-level `auth` / `dataApi`\n * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The\n * static half determines which secrets exist — so `NeonEnv<typeof config>` and `parseEnv`\n * are exact — while the closure can only *tune* a branch (lifecycle, compute, per-function\n * deploy settings), never change what exists.\n *\n * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch\n * being decided for (not a live handle); switch on its facts (`branch.name`,\n * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two\n * modes: against an existing branch (fields populated from Neon) and during pre-create\n * evaluation (`exists: false`, `id` undefined).\n *\n * Pure: no I/O, no side effects. The static parts are validated here; the closure's output\n * is validated every time it is evaluated so errors point at the concrete branch target.\n */\nexport function defineConfig<\n\tconst Auth extends ServiceToggleInput | undefined = undefined,\n\tconst DataApi extends DataApiInput | undefined = undefined,\n\tconst Preview extends PreviewInput | undefined = undefined,\n>(input: {\n\t// Each field is intersected with its concrete interface (not just typed as the bare\n\t// generic). The generic alone — e.g. `preview?: Preview` — gives editors no members to\n\t// complete against in the object-literal position (they see `{} | undefined`), so you\n\t// lose hints for `aiGateway` / `functions` / `buckets`. `& PreviewInput` restores the\n\t// full shape for autocomplete while still inferring the `const` literal that types the\n\t// `branch` closure's slugs (BranchTuningFn<Preview>) and the returned Config.\n\tauth?: Auth & ServiceToggleInput;\n\t// The `dataApi` field carries the Neon-Auth cross-field guard at the type level (see\n\t// `DataApiField`): a Neon-Auth Data API without `auth` enabled surfaces a readable hint\n\t// as the field's expected type instead of collapsing the value to `never`.\n\tdataApi?: DataApiField<Auth, DataApi>;\n\t// `& PreviewInput` restores top-level member hints (aiGateway/functions/buckets);\n\t// `& PreviewAutocomplete<Preview>` restores hints *inside* each function/bucket slug\n\t// object (see `PreviewAutocomplete`), which the bare index signature otherwise hides.\n\tpreview?: Preview & PreviewInput & PreviewAutocomplete<Preview>;\n\tbranch?: BranchTuningFn<Preview>;\n}): Config<Auth, DataApi, Preview> {\n\tif (typeof input === \"function\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, preview: { … }, branch: (branch) => ({ … }) })`.\",\n\t\t\t\"The static services/preview set moved to the top level; per-branch logic moved into the `branch` closure.\",\n\t\t]);\n\t}\n\tif (input === null || typeof input !== \"object\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig expects a configuration object: `export default defineConfig({ … })`.\",\n\t\t]);\n\t}\n\n\tconst parsed = configInputSchema.safeParse(input);\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\n\treturn Object.freeze({ ...input }) as Config<Auth, DataApi, Preview>;\n}\n\n/**\n * Evaluate a branch policy for a specific branch target and return a normalized config.\n *\n * Merges the static existential set (services + preview functions/buckets) with the\n * per-branch tuning returned by the `branch` closure into the same {@link\n * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.\n */\nexport function resolveConfig(\n\tconfig: Config,\n\tbranch: BranchTarget,\n): ResolvedBranchConfig {\n\tconst tuning = evaluateBranchTuning(config.branch, branch);\n\n\tconst resolved: ResolvedBranchConfig = {\n\t\tauthEnabled: isServiceEnabled(config.auth),\n\t\tdataApiEnabled: isDataApiEnabled(config.dataApi),\n\t\tdataApiPolicy:\n\t\t\tconfig.dataApi === undefined\n\t\t\t\t? \"omitted\"\n\t\t\t\t: isDataApiEnabled(config.dataApi)\n\t\t\t\t\t? \"enabled\"\n\t\t\t\t\t: \"disabled\",\n\t};\n\tconst dataApi = resolveDataApi(config.dataApi);\n\tif (dataApi) resolved.dataApi = dataApi;\n\tif (tuning.parent !== undefined) resolved.parent = tuning.parent;\n\tif (tuning.ttl !== undefined) {\n\t\t// `branchTuningSchema` already validated `ttl` with the same `parseBranchTtl`, so\n\t\t// this only converts the validated value to seconds — it cannot fail here.\n\t\tconst parsedTtl = parseBranchTtl(tuning.ttl);\n\t\tif (!(\"error\" in parsedTtl)) resolved.ttlSeconds = parsedTtl.seconds;\n\t}\n\tif (tuning.protected !== undefined) resolved.protected = tuning.protected;\n\tif (tuning.postgres?.computeSettings) {\n\t\tresolved.postgres = {\n\t\t\tcomputeSettings: { ...tuning.postgres.computeSettings },\n\t\t};\n\t}\n\n\tconst preview = resolvePreviewConfig(config.preview, tuning, branch);\n\tif (preview) resolved.preview = preview;\n\n\treturn resolved;\n}\n\n/**\n * Run the `branch` closure (when present) for the target and validate its output. The\n * closure is optional — a fully static policy resolves with empty tuning.\n */\nfunction evaluateBranchTuning(\n\tbranchFn: BranchTuningFn | undefined,\n\ttarget: BranchTarget,\n): BranchTuning {\n\tif (!branchFn) return {};\n\tlet raw: unknown;\n\ttry {\n\t\traw = branchFn(Object.freeze({ ...target }));\n\t} catch (cause) {\n\t\tthrow new ConfigValidationError([\n\t\t\t`Branch policy threw while evaluating branch \"${target.name}\".`,\n\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t]);\n\t}\n\tconst parsed = branchTuningSchema.safeParse(raw ?? {});\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\treturn parsed.data as BranchTuning;\n}\n\nfunction isServiceEnabled(toggle: ServiceToggleInput | undefined): boolean {\n\tif (toggle === undefined) return false;\n\tif (typeof toggle === \"boolean\") return toggle;\n\treturn toggle.enabled !== false;\n}\n\n/** Whether a {@link DataApiInput} is enabled (present object/`true` unless `enabled: false`). */\nfunction isDataApiEnabled(input: DataApiInput | undefined): boolean {\n\tif (input === undefined) return false;\n\tif (typeof input === \"boolean\") return input;\n\treturn input.enabled !== false;\n}\n\n/**\n * Normalize a {@link DataApiInput} into a {@link ResolvedDataApiConfig}, or `undefined` when\n * the Data API is not enabled. `authProvider` defaults to `\"neon\"`; the external-IdP wiring\n * is carried through only for the `\"external\"` provider; `settings` is copied with its\n * `undefined` entries dropped so diffing only considers fields the policy actually set.\n */\nfunction resolveDataApi(\n\tinput: DataApiInput | undefined,\n): ResolvedDataApiConfig | undefined {\n\tif (!isDataApiEnabled(input)) return undefined;\n\tif (typeof input !== \"object\") {\n\t\t// Bare `true`: enabled with Neon Auth and all-default settings.\n\t\treturn { authProvider: \"neon\" };\n\t}\n\tconst authProvider = input.authProvider ?? \"neon\";\n\tconst resolved: ResolvedDataApiConfig = { authProvider };\n\tif (authProvider === \"external\") {\n\t\tif (input.jwksUrl !== undefined) resolved.jwksUrl = input.jwksUrl;\n\t\tif (input.providerName !== undefined)\n\t\t\tresolved.providerName = input.providerName;\n\t\tif (input.jwtAudience !== undefined)\n\t\t\tresolved.jwtAudience = input.jwtAudience;\n\t}\n\tconst settings = normalizeDataApiSettings(input.settings);\n\tif (settings) resolved.settings = settings;\n\treturn resolved;\n}\n\n/** Copy a {@link DataApiSettings}, dropping `undefined` entries; `undefined` when empty. */\nfunction normalizeDataApiSettings(\n\tsettings: DataApiSettings | undefined,\n): DataApiSettings | undefined {\n\tif (!settings) return undefined;\n\tconst out: DataApiSettings = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\t(out as Record<string, unknown>)[key] = value;\n\t\t}\n\t}\n\treturn Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the static {@link PreviewInput} (merged with per-branch function tuning) into a\n * {@link ResolvedPreviewConfig}. Returns `undefined` when the policy declares no `preview`\n * block so the field can be omitted entirely. Function slugs / bucket names come from the\n * record keys.\n */\nfunction resolvePreviewConfig(\n\tpreview: PreviewInput | undefined,\n\ttuning: BranchTuning,\n\tbranch: BranchTarget,\n): ResolvedPreviewConfig | undefined {\n\tif (!preview) return undefined;\n\tconst fnTuning = tuning.preview?.functions ?? {};\n\tconst functions: ResolvedFunctionConfig[] = Object.entries(\n\t\tpreview.functions ?? {},\n\t).map(([slug, def]) =>\n\t\tresolveFunctionConfig(slug, def, fnTuning[slug] ?? {}, branch),\n\t);\n\tassertUniqueResolvedCustomDomains(functions);\n\tconst buckets = Object.entries(preview.buckets ?? {}).map(\n\t\t([name, def]) => ({\n\t\t\tname,\n\t\t\taccess: def.access ?? \"private\",\n\t\t}),\n\t);\n\treturn {\n\t\tfunctions,\n\t\tbuckets,\n\t\taiGatewayEnabled: isServiceEnabled(preview.aiGateway),\n\t};\n}\n\nfunction resolveFunctionConfig(\n\tslug: string,\n\tdef: FunctionDef,\n\ttuning: FunctionTuning,\n\tbranch: BranchTarget,\n): ResolvedFunctionConfig {\n\tconst customDomains = resolveFunctionCustomDomains(def, tuning, branch);\n\treturn {\n\t\tslug,\n\t\tname: def.name,\n\t\tsource: def.source,\n\t\tenv: { ...(def.env ?? {}) },\n\t\truntime: tuning.runtime ?? DEFAULT_FUNCTION_RUNTIME,\n\t\tbundler: def.bundler ?? DEFAULT_FUNCTION_BUNDLER,\n\t\t// Normalized only when declared, so a policy without it resolves unchanged and takes\n\t\t// the pre-existing bundling path. Both bundlers read it; `neon dev` mirrors it so a\n\t\t// local run leaves the same packages unbundled as a deploy.\n\t\t...(def.externalPackages\n\t\t\t? {\n\t\t\t\t\texternalPackages: def.externalPackages.map(\n\t\t\t\t\t\tnormalizeExternalPackage,\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t// Passed through untouched (no defaults); only `neon dev` reads it.\n\t\t...(def.dev ? { dev: def.dev } : {}),\n\t\t...(def.triggers\n\t\t\t? {\n\t\t\t\t\ttriggers: def.triggers.map((trigger) => ({\n\t\t\t\t\t\ttype: \"schedule\" as const,\n\t\t\t\t\t\tname: trigger.name,\n\t\t\t\t\t\tcron: trigger.cron,\n\t\t\t\t\t\tfunctionPath: trigger.functionPath ?? \"/\",\n\t\t\t\t\t\tenabled: trigger.enabled ?? true,\n\t\t\t\t\t})),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t...(customDomains !== undefined ? { customDomains } : {}),\n\t};\n}\n\nfunction resolveFunctionCustomDomains(\n\tdef: FunctionDef,\n\ttuning: FunctionTuning,\n\tbranch: BranchTarget,\n): string[] | undefined {\n\tif (tuning.customDomains !== undefined) {\n\t\treturn tuning.customDomains.map(normalizeCustomDomain);\n\t}\n\t// Hostnames are globally unique, so a child checkout 409s if it inherits this list.\n\tif (def.customDomains !== undefined && branch.isDefault === true) {\n\t\treturn def.customDomains.map(normalizeCustomDomain);\n\t}\n\treturn undefined;\n}\n\nfunction assertUniqueResolvedCustomDomains(\n\tfunctions: ResolvedFunctionConfig[],\n): void {\n\tconst byDomain = new Map<string, string>();\n\tfor (const fn of functions) {\n\t\tfor (const domain of fn.customDomains ?? []) {\n\t\t\tconst prior = byDomain.get(domain);\n\t\t\tif (prior !== undefined) {\n\t\t\t\tthrow new ConfigValidationError([\n\t\t\t\t\t`custom domain \"${domain}\" is used by both function \"${prior}\" and \"${fn.slug}\"`,\n\t\t\t\t]);\n\t\t\t}\n\t\t\tbyDomain.set(domain, fn.slug);\n\t\t}\n\t}\n}\n\n/**\n * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes\n * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the\n * NeonApi adapter.\n */\nexport function normalizeRegion(region: string): string {\n\tif (REGION_PREFIX.test(region)) return region;\n\treturn `aws-${region}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import {\n\tauthoredFunctionTuning,\n\tmergeAuthoredPreview,\n} from \"./authored-services.js\";\nimport { normalizeCustomDomain } from \"./custom-domain.js\";\nimport { parseBranchTtl } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport { normalizeExternalPackage } from \"./external-packages.js\";\nimport {\n\tbranchTuningSchema,\n\tconfigInputSchema,\n\tformatZodIssues,\n} from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tBranchTuning,\n\tBranchTuningFn,\n\tBucketDef,\n\tConfig,\n\tDataApiInput,\n\tDataApiSettings,\n\tFunctionDef,\n\tFunctionTuning,\n\tPreviewInput,\n\tResolvedBranchConfig,\n\tResolvedDataApiConfig,\n\tResolvedFunctionConfig,\n\tResolvedPreviewConfig,\n\tServiceEnabled,\n\tServiceToggleInput,\n} from \"./types.js\";\n\n/** Default deploy parameters applied to functions that omit them in `neon.ts`. */\nconst DEFAULT_FUNCTION_RUNTIME = \"nodejs24\" as const;\n\nconst DEFAULT_FUNCTION_BUNDLER = \"esbuild\" as const;\n\nconst REGION_PREFIX = /^(aws|azure|gcp)-/;\n\n/**\n * Whether a `dataApi` toggle is **enabled and verified by Neon Auth** at the type level: it is\n * on (see {@link ServiceEnabled}) and not the explicit `authProvider: \"external\"` variant\n * (so the default / `\"neon\"` provider). This is the case that requires top-level Neon Auth.\n */\ntype DataApiUsesNeonAuth<DataApi> =\n\tServiceEnabled<DataApi> extends true\n\t\t? [DataApi] extends [{ authProvider: \"external\" }]\n\t\t\t? false\n\t\t\t: true\n\t\t: false;\n\n/**\n * Human-readable hint surfaced as the **expected type** of `dataApi` when a Neon-Auth Data\n * API is declared without Neon Auth enabled (see {@link DataApiField}). TypeScript prints the\n * offending value against this string literal — `Type 'true' is not assignable to type\n * '…requires `auth: true`…'` — which points straight at the fix, instead of the opaque\n * `Type 'true' is not assignable to type 'never'` an intersection guard produces.\n *\n * It documents **both** fixes: enabling Neon Auth (`auth: true`), and running the Data API\n * *without* Neon Auth by verifying a third-party IdP (`authProvider: 'external'` + `jwksUrl`).\n */\n// Exported (type-only) for the type tests in `define-config.test-d.ts`; intentionally not\n// re-exported from `v1.ts` / `index.ts`, so it stays an internal implementation detail.\nexport type NeonAuthRequiredHint =\n\t\"`dataApi` with Neon Auth (the default `authProvider: 'neon'`) requires Neon Auth, so add `auth: true`. To enable the Data API WITHOUT Neon Auth, verify a third-party IdP instead: `dataApi: { authProvider: 'external', jwksUrl: 'https://your-idp/.well-known/jwks.json' }`\";\n\n/**\n * Static cross-field guard for {@link defineConfig}, expressed as the **type of the `dataApi`\n * field** rather than an intersected requirement on `auth`.\n *\n * - A Neon-Auth Data API (`authProvider: \"neon\"`, the default) with top-level `auth` enabled,\n * or any external Data API: the field keeps its normal `DataApi & DataApiInput` type (the\n * `& DataApiInput` preserves member autocomplete; the `const DataApi` still types the\n * returned {@link Config}).\n * - A Neon-Auth Data API **without** `auth` enabled: the field's expected type collapses to\n * the {@link NeonAuthRequiredHint} message, so the author sees the rule (and the two fixes)\n * right on the `dataApi` value.\n *\n * The runtime `superRefine` in {@link configInputSchema} enforces the same invariant for\n * non-typed (plain-JS) callers, so the behavior is identical — only the type-level message\n * changes.\n */\n// Exported (type-only) for the type tests in `define-config.test-d.ts`; intentionally not\n// re-exported from `v1.ts` / `index.ts`, so it stays an internal implementation detail.\nexport type DataApiField<Auth, DataApi> =\n\tDataApiUsesNeonAuth<DataApi> extends true\n\t\t? ServiceEnabled<Auth> extends true\n\t\t\t? DataApi & DataApiInput\n\t\t\t: NeonAuthRequiredHint\n\t\t: DataApi & DataApiInput;\n\n/**\n * Autocomplete bridge for the nested `preview.functions` / `preview.buckets` slug objects.\n *\n * {@link PreviewInput} types those records with a string index signature\n * (`Record<string, FunctionDef>` / `Record<string, BucketDef>`). When `defineConfig` infers\n * `const Preview`, every authored slug becomes a **named** property on the inferred literal\n * (e.g. `{ hello: { name; source } }`), and a named property **shadows** the index signature\n * when the editor computes the contextual type of that slug's value — so the rest of\n * {@link FunctionDef} / {@link BucketDef} (`env`, `dev`, `access`, …) never surfaces as\n * completions inside `hello: { … }` / `uploads: { … }`.\n *\n * Re-declaring each inferred slug's value as `FunctionDef` / `BucketDef` (a *named* member, via\n * a mapped type over the already-inferred keys) puts those members back onto the contextual\n * type without going through an index signature, which restores autocomplete. Intersected with\n * `Preview & PreviewInput` it neither widens what is accepted (the values were already\n * `FunctionDef` / `BucketDef`) nor perturbs the inferred `const Preview` — so slug inference for\n * `BranchTuningFn<Preview>` and the returned {@link Config} is unchanged.\n */\ntype PreviewAutocomplete<Preview> = (Preview extends { functions: infer F }\n\t? { functions: { [Slug in keyof F]: FunctionDef } }\n\t: unknown) &\n\t(Preview extends { buckets: infer B }\n\t\t? { buckets: { [Name in keyof B]: BucketDef } }\n\t\t: unknown);\n\ntype FunctionsAutocomplete<Functions> =\n\tFunctions extends Record<string, unknown>\n\t\t? { [Slug in keyof Functions]: FunctionDef }\n\t\t: unknown;\n\ntype BucketsAutocomplete<Buckets> =\n\tBuckets extends Record<string, unknown>\n\t\t? { [Name in keyof Buckets]: BucketDef }\n\t\t: unknown;\n\n/**\n * Validate and freeze a Neon branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neon/config/v1\";\n *\n * export default defineConfig({\n * auth: true,\n * functions: {\n * hello: { name: \"Hello\", source: \"./functions/hello.ts\", dev: { port: 8787 } },\n * },\n * branch: (branch) => ({ protected: branch.name === \"main\" }),\n * });\n * ```\n *\n * The policy is split into a **static** existential set (top-level `auth` / `dataApi` /\n * `aiGateway` / `functions` / `buckets`, plus the deprecated `preview` aliases) and a\n * **dynamic** per-branch `branch` closure. The static half determines which secrets exist —\n * so `NeonEnv<typeof config>` and `parseEnv` are exact — while the closure can only *tune*\n * a branch (lifecycle, compute, per-function deploy settings), never change what exists.\n *\n * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch\n * being decided for (not a live handle); switch on its facts (`branch.name`,\n * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two\n * modes: against an existing branch (fields populated from Neon) and during pre-create\n * evaluation (`exists: false`, `id` undefined).\n *\n * Pure: no I/O, no side effects. The static parts are validated here; the closure's output\n * is validated every time it is evaluated so errors point at the concrete branch target.\n */\nexport function defineConfig<\n\tconst Auth extends ServiceToggleInput | undefined = undefined,\n\tconst DataApi extends DataApiInput | undefined = undefined,\n\tconst Preview extends PreviewInput | undefined = undefined,\n\tconst Functions extends Record<string, FunctionDef> | undefined = undefined,\n\tconst Buckets extends Record<string, BucketDef> | undefined = undefined,\n\tconst AiGateway extends ServiceToggleInput | undefined = undefined,\n>(input: {\n\t// Each field is intersected with its concrete interface (not just typed as the bare\n\t// generic). The generic alone — e.g. `preview?: Preview` — gives editors no members to\n\t// complete against in the object-literal position (they see `{} | undefined`), so you\n\t// lose hints for `aiGateway` / `functions` / `buckets`. `& PreviewInput` restores the\n\t// full shape for autocomplete while still inferring the `const` literal that types the\n\t// `branch` closure's slugs (BranchTuningFn<Preview>) and the returned Config.\n\tauth?: Auth & ServiceToggleInput;\n\t// The `dataApi` field carries the Neon-Auth cross-field guard at the type level (see\n\t// `DataApiField`): a Neon-Auth Data API without `auth` enabled surfaces a readable hint\n\t// as the field's expected type instead of collapsing the value to `never`.\n\tdataApi?: DataApiField<Auth, DataApi>;\n\taiGateway?: AiGateway & ServiceToggleInput;\n\tfunctions?: Functions &\n\t\tRecord<string, FunctionDef> &\n\t\tFunctionsAutocomplete<Functions>;\n\tbuckets?: Buckets &\n\t\tRecord<string, BucketDef> &\n\t\tBucketsAutocomplete<Buckets>;\n\t// `& PreviewInput` restores top-level member hints (aiGateway/functions/buckets);\n\t// `& PreviewAutocomplete<Preview>` restores hints *inside* each function/bucket slug\n\t// object (see `PreviewAutocomplete`), which the bare index signature otherwise hides.\n\t/** @deprecated Use top-level `aiGateway`, `functions`, and `buckets`. */\n\tpreview?: Preview & PreviewInput & PreviewAutocomplete<Preview>;\n\tbranch?: BranchTuningFn<Preview, Functions>;\n}): Config<Auth, DataApi, Preview, Functions, Buckets, AiGateway> {\n\tif (typeof input === \"function\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig now expects an object, not a function: `export default defineConfig({ auth: true, functions: { … }, branch: (branch) => ({ … }) })`.\",\n\t\t\t\"The static services set moved to the top level; per-branch logic moved into the `branch` closure.\",\n\t\t]);\n\t}\n\tif (input === null || typeof input !== \"object\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig expects a configuration object: `export default defineConfig({ … })`.\",\n\t\t]);\n\t}\n\n\tconst parsed = configInputSchema.safeParse(input);\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\n\treturn Object.freeze({ ...input }) as Config<\n\t\tAuth,\n\t\tDataApi,\n\t\tPreview,\n\t\tFunctions,\n\t\tBuckets,\n\t\tAiGateway\n\t>;\n}\n\n/**\n * Evaluate a branch policy for a specific branch target and return a normalized config.\n *\n * Merges the static existential set (services + functions/buckets) with the\n * per-branch tuning returned by the `branch` closure into the same {@link\n * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.\n */\nexport function resolveConfig(\n\tconfig: Config,\n\tbranch: BranchTarget,\n): ResolvedBranchConfig {\n\tconst tuning = evaluateBranchTuning(config.branch, branch);\n\n\tconst resolved: ResolvedBranchConfig = {\n\t\tauthEnabled: isServiceEnabled(config.auth),\n\t\tdataApiEnabled: isDataApiEnabled(config.dataApi),\n\t\tdataApiPolicy:\n\t\t\tconfig.dataApi === undefined\n\t\t\t\t? \"omitted\"\n\t\t\t\t: isDataApiEnabled(config.dataApi)\n\t\t\t\t\t? \"enabled\"\n\t\t\t\t\t: \"disabled\",\n\t};\n\tconst dataApi = resolveDataApi(config.dataApi);\n\tif (dataApi) resolved.dataApi = dataApi;\n\tif (tuning.parent !== undefined) resolved.parent = tuning.parent;\n\tif (tuning.ttl !== undefined) {\n\t\t// `branchTuningSchema` already validated `ttl` with the same `parseBranchTtl`, so\n\t\t// this only converts the validated value to seconds — it cannot fail here.\n\t\tconst parsedTtl = parseBranchTtl(tuning.ttl);\n\t\tif (!(\"error\" in parsedTtl)) resolved.ttlSeconds = parsedTtl.seconds;\n\t}\n\tif (tuning.protected !== undefined) resolved.protected = tuning.protected;\n\tif (tuning.postgres?.computeSettings) {\n\t\tresolved.postgres = {\n\t\t\tcomputeSettings: { ...tuning.postgres.computeSettings },\n\t\t};\n\t}\n\n\tconst preview = resolvePreviewConfig(\n\t\tmergeAuthoredPreview(config),\n\t\ttuning,\n\t\tbranch,\n\t);\n\tif (preview) resolved.preview = preview;\n\n\treturn resolved;\n}\n\n/**\n * Run the `branch` closure (when present) for the target and validate its output. The\n * closure is optional — a fully static policy resolves with empty tuning.\n */\nfunction evaluateBranchTuning(\n\tbranchFn: BranchTuningFn | undefined,\n\ttarget: BranchTarget,\n): BranchTuning {\n\tif (!branchFn) return {};\n\tlet raw: unknown;\n\ttry {\n\t\traw = branchFn(Object.freeze({ ...target }));\n\t} catch (cause) {\n\t\tthrow new ConfigValidationError([\n\t\t\t`Branch policy threw while evaluating branch \"${target.name}\".`,\n\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t]);\n\t}\n\tconst parsed = branchTuningSchema.safeParse(raw ?? {});\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\treturn parsed.data as BranchTuning;\n}\n\nfunction isServiceEnabled(toggle: ServiceToggleInput | undefined): boolean {\n\tif (toggle === undefined) return false;\n\tif (typeof toggle === \"boolean\") return toggle;\n\treturn toggle.enabled !== false;\n}\n\n/** Whether a {@link DataApiInput} is enabled (present object/`true` unless `enabled: false`). */\nfunction isDataApiEnabled(input: DataApiInput | undefined): boolean {\n\tif (input === undefined) return false;\n\tif (typeof input === \"boolean\") return input;\n\treturn input.enabled !== false;\n}\n\n/**\n * Normalize a {@link DataApiInput} into a {@link ResolvedDataApiConfig}, or `undefined` when\n * the Data API is not enabled. `authProvider` defaults to `\"neon\"`; the external-IdP wiring\n * is carried through only for the `\"external\"` provider; `settings` is copied with its\n * `undefined` entries dropped so diffing only considers fields the policy actually set.\n */\nfunction resolveDataApi(\n\tinput: DataApiInput | undefined,\n): ResolvedDataApiConfig | undefined {\n\tif (!isDataApiEnabled(input)) return undefined;\n\tif (typeof input !== \"object\") {\n\t\t// Bare `true`: enabled with Neon Auth and all-default settings.\n\t\treturn { authProvider: \"neon\" };\n\t}\n\tconst authProvider = input.authProvider ?? \"neon\";\n\tconst resolved: ResolvedDataApiConfig = { authProvider };\n\tif (authProvider === \"external\") {\n\t\tif (input.jwksUrl !== undefined) resolved.jwksUrl = input.jwksUrl;\n\t\tif (input.providerName !== undefined)\n\t\t\tresolved.providerName = input.providerName;\n\t\tif (input.jwtAudience !== undefined)\n\t\t\tresolved.jwtAudience = input.jwtAudience;\n\t}\n\tconst settings = normalizeDataApiSettings(input.settings);\n\tif (settings) resolved.settings = settings;\n\treturn resolved;\n}\n\n/** Copy a {@link DataApiSettings}, dropping `undefined` entries; `undefined` when empty. */\nfunction normalizeDataApiSettings(\n\tsettings: DataApiSettings | undefined,\n): DataApiSettings | undefined {\n\tif (!settings) return undefined;\n\tconst out: DataApiSettings = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\t(out as Record<string, unknown>)[key] = value;\n\t\t}\n\t}\n\treturn Object.keys(out).length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the static {@link PreviewInput} (merged with per-branch function tuning) into a\n * {@link ResolvedPreviewConfig}. Returns `undefined` when the policy declares no `preview`\n * block so the field can be omitted entirely. Function slugs / bucket names come from the\n * record keys.\n */\nfunction resolvePreviewConfig(\n\tpreview: PreviewInput | undefined,\n\ttuning: BranchTuning,\n\tbranch: BranchTarget,\n): ResolvedPreviewConfig | undefined {\n\tif (!preview) return undefined;\n\tconst fnTuning = authoredFunctionTuning(tuning);\n\tconst functions: ResolvedFunctionConfig[] = Object.entries(\n\t\tpreview.functions ?? {},\n\t).map(([slug, def]) =>\n\t\tresolveFunctionConfig(slug, def, fnTuning[slug] ?? {}, branch),\n\t);\n\tassertUniqueResolvedCustomDomains(functions);\n\tconst buckets = Object.entries(preview.buckets ?? {}).map(\n\t\t([name, def]) => ({\n\t\t\tname,\n\t\t\taccess: def.access ?? \"private\",\n\t\t}),\n\t);\n\treturn {\n\t\tfunctions,\n\t\tbuckets,\n\t\taiGatewayEnabled: isServiceEnabled(preview.aiGateway),\n\t};\n}\n\nfunction resolveFunctionConfig(\n\tslug: string,\n\tdef: FunctionDef,\n\ttuning: FunctionTuning,\n\tbranch: BranchTarget,\n): ResolvedFunctionConfig {\n\tconst customDomains = resolveFunctionCustomDomains(def, tuning, branch);\n\treturn {\n\t\tslug,\n\t\tname: def.name,\n\t\tsource: def.source,\n\t\tenv: { ...(def.env ?? {}) },\n\t\truntime: tuning.runtime ?? DEFAULT_FUNCTION_RUNTIME,\n\t\tbundler: def.bundler ?? DEFAULT_FUNCTION_BUNDLER,\n\t\t// Normalized only when declared, so a policy without it resolves unchanged and takes\n\t\t// the pre-existing bundling path. Both bundlers read it; `neon dev` mirrors it so a\n\t\t// local run leaves the same packages unbundled as a deploy.\n\t\t...(def.externalPackages\n\t\t\t? {\n\t\t\t\t\texternalPackages: def.externalPackages.map(\n\t\t\t\t\t\tnormalizeExternalPackage,\n\t\t\t\t\t),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t// Passed through untouched (no defaults); only `neon dev` reads it.\n\t\t...(def.dev ? { dev: def.dev } : {}),\n\t\t...(def.triggers\n\t\t\t? {\n\t\t\t\t\ttriggers: def.triggers.map((trigger) => ({\n\t\t\t\t\t\ttype: \"schedule\" as const,\n\t\t\t\t\t\tname: trigger.name,\n\t\t\t\t\t\tcron: trigger.cron,\n\t\t\t\t\t\tfunctionPath: trigger.functionPath ?? \"/\",\n\t\t\t\t\t\tenabled: trigger.enabled ?? true,\n\t\t\t\t\t})),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t...(customDomains !== undefined ? { customDomains } : {}),\n\t};\n}\n\nfunction resolveFunctionCustomDomains(\n\tdef: FunctionDef,\n\ttuning: FunctionTuning,\n\tbranch: BranchTarget,\n): string[] | undefined {\n\tif (tuning.customDomains !== undefined) {\n\t\treturn tuning.customDomains.map(normalizeCustomDomain);\n\t}\n\t// Hostnames are globally unique, so a child checkout 409s if it inherits this list.\n\tif (def.customDomains !== undefined && branch.isDefault === true) {\n\t\treturn def.customDomains.map(normalizeCustomDomain);\n\t}\n\treturn undefined;\n}\n\nfunction assertUniqueResolvedCustomDomains(\n\tfunctions: ResolvedFunctionConfig[],\n): void {\n\tconst byDomain = new Map<string, string>();\n\tfor (const fn of functions) {\n\t\tfor (const domain of fn.customDomains ?? []) {\n\t\t\tconst prior = byDomain.get(domain);\n\t\t\tif (prior !== undefined) {\n\t\t\t\tthrow new ConfigValidationError([\n\t\t\t\t\t`custom domain \"${domain}\" is used by both function \"${prior}\" and \"${fn.slug}\"`,\n\t\t\t\t]);\n\t\t\t}\n\t\t\tbyDomain.set(domain, fn.slug);\n\t\t}\n\t}\n}\n\n/**\n * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes\n * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the\n * NeonApi adapter.\n */\nexport function normalizeRegion(region: string): string {\n\tif (REGION_PREFIX.test(region)) return region;\n\treturn `aws-${region}`;\n}\n"],"mappings":"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"}
@@ -133,7 +133,7 @@ function safeIsFile(path) {
133
133
  return false;
134
134
  }
135
135
  }
136
- const UNSET_FUNCTION_ENV_ISSUE = /^preview\.functions\.[^.]+\.env\.(.+): Environment variable "\1" for function ".+" is undefined/;
136
+ const UNSET_FUNCTION_ENV_ISSUE = /^(?:preview\.)?functions\.[^.]+\.env\.(.+): Environment variable "\1" for function ".+" is undefined/;
137
137
  function unsetFunctionEnvKeys(err) {
138
138
  if (!isPlatformError(err) || err.code !== ErrorCode.InvalidConfig) return null;
139
139
  const issues = platformErrorIssues(err);
@@ -180,7 +180,7 @@ async function withSerializedConfigLoad(run) {
180
180
  return next;
181
181
  }
182
182
  function withoutFunctionEnvKeys(config, keys) {
183
- const functions = config.preview?.functions;
183
+ const functions = config.functions ?? config.preview?.functions;
184
184
  if (!functions) return config;
185
185
  const nextFunctions = {};
186
186
  for (const [slug, fn] of Object.entries(functions)) {
@@ -194,6 +194,10 @@ function withoutFunctionEnvKeys(config, keys) {
194
194
  env
195
195
  };
196
196
  }
197
+ if (config.functions !== void 0) return Object.freeze({
198
+ ...config,
199
+ functions: nextFunctions
200
+ });
197
201
  return Object.freeze({
198
202
  ...config,
199
203
  preview: {