@neondatabase/config 0.2.1 → 0.4.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
@@ -16,17 +16,30 @@ npm install @neondatabase/config
16
16
  // neon.ts
17
17
  import { defineConfig } from "@neondatabase/config/v1";
18
18
 
19
- export default defineConfig((branch) => {
20
- if (branch.name === "main") {
21
- return { protected: true, auth: {} };
22
- }
23
- return { parent: "main", ttl: "7d" };
19
+ export default defineConfig({
20
+ // Static: what *exists* on every branch. GA service toggles drive the typed env.
21
+ auth: true,
22
+ dataApi: false,
23
+ // Beta (Preview) features, keyed by slug / name.
24
+ preview: {
25
+ functions: {
26
+ hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
27
+ },
28
+ },
29
+ // Dynamic: per-branch tuning only. Cannot add/remove services or functions.
30
+ branch: (branch) => ({
31
+ protected: branch.name === "main",
32
+ ...(branch.name === "main" ? {} : { parent: "main", ttl: "7d" }),
33
+ }),
24
34
  });
25
35
  ```
26
36
 
27
- The `branch` argument is a **read-only descriptor** (`BranchTarget`) of the branch this policy is being evaluated for — `name`, `id`, `exists`, `isDefault`, `isProtected`, `parentId`, `expiresAt`. It is not a live branch handle: don't mutate it, just switch on its fields and **return** the desired config. The same callback runs both against existing branches and during pre-create evaluation (`exists: false`).
37
+ A policy is split into a **static** existential set and a **dynamic** `branch` closure:
28
38
 
29
- `parent` and `ttl` are branch lifecycle fields. Product-specific settings live under product namespaces such as `postgres`, `auth`, and `dataApi`.
39
+ - **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 `@neondatabase/env` return an exact `NeonEnv`.
40
+ - **`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`, and per-function `runtime`. 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.
41
+
42
+ Service toggles accept `true` / `{}` / `{ enabled: true }` (enabled) and `false` / `{ enabled: false }` (disabled). Function slugs (record keys) must match `^[a-z0-9]{1,20}$`.
30
43
 
31
44
  ## Functions
32
45
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AppliedChange, BranchConfig, BranchTarget, BucketAccessLevel, BucketConfig, ComputeSettings, Config, ConflictReport, FunctionConfig, FunctionDevConfig, FunctionMemoryMib, FunctionRuntime, PostgresConfig, PreviewConfig, PushResult, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle } from "./lib/types.js";
1
+ import { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, Config, ConflictReport, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, PostgresConfig, PreviewInput, PreviewTuning, PushResult, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, ServiceToggleInput } from "./lib/types.js";
2
2
  import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError } from "./lib/errors.js";
3
3
  import { CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, GetConnectionUriInput, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, UpdateBranchInput } from "./lib/neon-api.js";
4
4
  import { createNeonApiFromOptions, resolveApiKey } from "./lib/auth.js";
@@ -7,4 +7,4 @@ import { DiffOptions, DiffResult, PlanStep, RemotePreviewState, RemoteServiceSta
7
7
  import { LoadConfigOptions, loadConfigFromFile } from "./lib/loader.js";
8
8
  import { createRealNeonApi } from "./lib/neon-api-real.js";
9
9
  import { errors, schemas } from "./v1.js";
10
- export { AppliedChange, BranchConfig, BranchTarget, BucketAccessLevel, BucketConfig, ComputeSettings, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, DiffOptions, DiffResult, ErrorCode, FunctionConfig, FunctionDevConfig, FunctionMemoryMib, FunctionRuntime, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, PlanStep, PlatformError, PostgresConfig, PreviewConfig, PushAbortedError, PushConflictError, PushResult, RemotePreviewState, RemoteServiceState, RemoteState, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, UpdateBranchInput, createNeonApiFromOptions, createRealNeonApi, defineConfig, diffConfig, errors, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
10
+ export { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, DiffOptions, DiffResult, ErrorCode, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, PlanStep, PlatformError, PostgresConfig, PreviewInput, PreviewTuning, PushAbortedError, PushConflictError, PushResult, RemotePreviewState, RemoteServiceState, RemoteState, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, ServiceToggleInput, UpdateBranchInput, createNeonApiFromOptions, createRealNeonApi, defineConfig, diffConfig, errors, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
@@ -1,4 +1,4 @@
1
- import { BranchTarget, Config, ResolvedBranchConfig } from "./types.js";
1
+ import { BranchTarget, BranchTuningFn, Config, PreviewInput, ResolvedBranchConfig, ServiceToggleInput } from "./types.js";
2
2
 
3
3
  //#region src/lib/define-config.d.ts
4
4
 
@@ -9,27 +9,44 @@ import { BranchTarget, Config, ResolvedBranchConfig } from "./types.js";
9
9
  * ```ts
10
10
  * import { defineConfig } from "@neondatabase/config/v1";
11
11
  *
12
- * export default defineConfig((branch) => {
13
- * if (branch.name === "main") {
14
- * return { protected: true, auth: {} };
15
- * }
16
- * return { parent: "main", ttl: "7d" };
12
+ * export default defineConfig({
13
+ * auth: true,
14
+ * preview: {
15
+ * functions: {
16
+ * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
17
+ * },
18
+ * },
19
+ * branch: (branch) => ({ protected: branch.name === "main" }),
17
20
  * });
18
21
  * ```
19
22
  *
20
- * The `branch` parameter is a **read-only {@link BranchTarget} descriptor** of the branch
21
- * this policy invocation is deciding for not a live branch handle. You don't mutate it
22
- * (`branch.protected = true` does nothing); you switch on its facts (`branch.name`,
23
- * `branch.isDefault`, `branch.exists`, …) and **return** the desired {@link BranchConfig}.
24
- * The same callback runs in two modes: against an existing branch (fields populated from
25
- * Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
23
+ * The policy is split into a **static** existential set (top-level `auth` / `dataApi`
24
+ * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
25
+ * static half determines which secrets exist so `NeonEnv<typeof config>` and `parseEnv`
26
+ * are exact while the closure can only *tune* a branch (lifecycle, compute, per-function
27
+ * deploy settings), never change what exists.
26
28
  *
27
- * Pure function no I/O, no side effects. The returned policy validates its output every
28
- * time it is evaluated so errors point at the concrete branch target that triggered them.
29
+ * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
30
+ * being decided for (not a live handle); switch on its facts (`branch.name`,
31
+ * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two
32
+ * modes: against an existing branch (fields populated from Neon) and during pre-create
33
+ * evaluation (`exists: false`, `id` undefined).
34
+ *
35
+ * Pure: no I/O, no side effects. The static parts are validated here; the closure's output
36
+ * is validated every time it is evaluated so errors point at the concrete branch target.
29
37
  */
30
- declare function defineConfig<const C extends Config>(input: C): C;
38
+ declare function defineConfig<const Auth extends ServiceToggleInput | undefined = undefined, const DataApi extends ServiceToggleInput | undefined = undefined, const Preview extends PreviewInput | undefined = undefined>(input: {
39
+ auth?: Auth;
40
+ dataApi?: DataApi;
41
+ preview?: Preview;
42
+ branch?: BranchTuningFn<Preview>;
43
+ }): Config<Auth, DataApi, Preview>;
31
44
  /**
32
45
  * Evaluate a branch policy for a specific branch target and return a normalized config.
46
+ *
47
+ * Merges the static existential set (services + preview functions/buckets) with the
48
+ * per-branch tuning returned by the `branch` closure into the same {@link
49
+ * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
33
50
  */
34
51
  declare function resolveConfig(config: Config, branch: BranchTarget): ResolvedBranchConfig;
35
52
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;AA4CA;;;;;AAAiE;AAcjE;;;;;AAGuB;AA0FvB;;;;;;;;;;;iBA3GgB,6BAA6B,eAAe,IAAI;;;;iBAchD,aAAA,SACP,gBACA,eACN;;;;;;iBA0Fa,eAAA"}
1
+ {"version":3,"file":"define-config.d.ts","names":[],"sources":["../../src/lib/define-config.ts"],"mappings":";;;;;;AA2DA;;;;;;;;;;;;;;AASU;AA4BV;;;;;AAGuB;AA6GvB;;;;;;;;;;iBArJgB,gCACI,kEACG,kEACA;SAEf;YACG;YACA;WACD,eAAe;IACrB,OAAO,MAAM,SAAS;;;;;;;;iBA4BV,aAAA,SACP,gBACA,eACN;;;;;;iBA6Ga,eAAA"}
@@ -1,10 +1,9 @@
1
1
  import { ConfigValidationError } from "./errors.js";
2
2
  import { parseDuration } from "./duration.js";
3
- import { branchConfigSchema, formatZodIssues } from "./schema.js";
3
+ import { branchTuningSchema, configInputSchema, formatZodIssues } from "./schema.js";
4
4
  //#region src/lib/define-config.ts
5
5
  /** Default deploy parameters applied to functions that omit them in `neon.ts`. */
6
6
  const DEFAULT_FUNCTION_RUNTIME = "nodejs24";
7
- const DEFAULT_FUNCTION_MEMORY_MIB = 512;
8
7
  const REGION_PREFIX = /^(aws|azure|gcp)-/;
9
8
  /**
10
9
  * Validate and freeze a Neon Platform branch policy.
@@ -13,88 +12,110 @@ const REGION_PREFIX = /^(aws|azure|gcp)-/;
13
12
  * ```ts
14
13
  * import { defineConfig } from "@neondatabase/config/v1";
15
14
  *
16
- * export default defineConfig((branch) => {
17
- * if (branch.name === "main") {
18
- * return { protected: true, auth: {} };
19
- * }
20
- * return { parent: "main", ttl: "7d" };
15
+ * export default defineConfig({
16
+ * auth: true,
17
+ * preview: {
18
+ * functions: {
19
+ * hello: { name: "Hello", source: "./functions/hello.ts", dev: { port: 8787 } },
20
+ * },
21
+ * },
22
+ * branch: (branch) => ({ protected: branch.name === "main" }),
21
23
  * });
22
24
  * ```
23
25
  *
24
- * The `branch` parameter is a **read-only {@link BranchTarget} descriptor** of the branch
25
- * this policy invocation is deciding for not a live branch handle. You don't mutate it
26
- * (`branch.protected = true` does nothing); you switch on its facts (`branch.name`,
27
- * `branch.isDefault`, `branch.exists`, …) and **return** the desired {@link BranchConfig}.
28
- * The same callback runs in two modes: against an existing branch (fields populated from
29
- * Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
26
+ * The policy is split into a **static** existential set (top-level `auth` / `dataApi`
27
+ * toggles and the beta `preview` block) and a **dynamic** per-branch `branch` closure. The
28
+ * static half determines which secrets exist so `NeonEnv<typeof config>` and `parseEnv`
29
+ * are exact while the closure can only *tune* a branch (lifecycle, compute, per-function
30
+ * deploy settings), never change what exists.
30
31
  *
31
- * Pure function no I/O, no side effects. The returned policy validates its output every
32
- * time it is evaluated so errors point at the concrete branch target that triggered them.
32
+ * The `branch` callback receives a read-only {@link BranchTarget} descriptor of the branch
33
+ * being decided for (not a live handle); switch on its facts (`branch.name`,
34
+ * `branch.isDefault`, `branch.exists`, …) and **return** the desired tuning. It runs in two
35
+ * modes: against an existing branch (fields populated from Neon) and during pre-create
36
+ * evaluation (`exists: false`, `id` undefined).
37
+ *
38
+ * Pure: no I/O, no side effects. The static parts are validated here; the closure's output
39
+ * is validated every time it is evaluated so errors point at the concrete branch target.
33
40
  */
34
41
  function defineConfig(input) {
35
- if (typeof input !== "function") throw new ConfigValidationError(["defineConfig expects a function: `export default defineConfig((branch) => ({ ... }))`.", "Project-level config has moved to `neonctl link`; neon.ts now describes branch-level policy only."]);
36
- return Object.freeze(input);
42
+ 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."]);
43
+ if (input === null || typeof input !== "object") throw new ConfigValidationError(["defineConfig expects a configuration object: `export default defineConfig({ … })`."]);
44
+ const parsed = configInputSchema.safeParse(input);
45
+ if (!parsed.success) throw new ConfigValidationError(formatZodIssues(parsed.error));
46
+ return Object.freeze({ ...input });
37
47
  }
38
48
  /**
39
49
  * Evaluate a branch policy for a specific branch target and return a normalized config.
50
+ *
51
+ * Merges the static existential set (services + preview functions/buckets) with the
52
+ * per-branch tuning returned by the `branch` closure into the same {@link
53
+ * ResolvedBranchConfig} the rest of the runtime (diff / push / fetchEnv) consumes.
40
54
  */
41
55
  function resolveConfig(config, branch) {
56
+ const tuning = evaluateBranchTuning(config.branch, branch);
57
+ const resolved = {
58
+ authEnabled: isServiceEnabled(config.auth),
59
+ dataApiEnabled: isServiceEnabled(config.dataApi)
60
+ };
61
+ if (tuning.parent !== void 0) resolved.parent = tuning.parent;
62
+ if (tuning.ttl !== void 0) {
63
+ const parsedTtl = parseDuration(tuning.ttl);
64
+ if (!("error" in parsedTtl)) resolved.ttlSeconds = parsedTtl.seconds;
65
+ }
66
+ if (tuning.protected !== void 0) resolved.protected = tuning.protected;
67
+ if (tuning.postgres?.computeSettings) resolved.postgres = { computeSettings: { ...tuning.postgres.computeSettings } };
68
+ const preview = resolvePreviewConfig(config.preview, tuning);
69
+ if (preview) resolved.preview = preview;
70
+ return resolved;
71
+ }
72
+ /**
73
+ * Run the `branch` closure (when present) for the target and validate its output. The
74
+ * closure is optional — a fully static policy resolves with empty tuning.
75
+ */
76
+ function evaluateBranchTuning(branchFn, target) {
77
+ if (!branchFn) return {};
42
78
  let raw;
43
79
  try {
44
- raw = config(Object.freeze({ ...branch }));
80
+ raw = branchFn(Object.freeze({ ...target }));
45
81
  } catch (cause) {
46
- throw new ConfigValidationError([`Config function threw while evaluating branch "${branch.name}".`, cause?.message ?? String(cause)]);
82
+ throw new ConfigValidationError([`Branch policy threw while evaluating branch "${target.name}".`, cause?.message ?? String(cause)]);
47
83
  }
48
- const parsed = branchConfigSchema.safeParse(raw);
84
+ const parsed = branchTuningSchema.safeParse(raw ?? {});
49
85
  if (!parsed.success) throw new ConfigValidationError(formatZodIssues(parsed.error));
50
- const cfg = parsed.data;
51
- const issues = [];
52
- let ttlSeconds;
53
- if (cfg.ttl !== void 0) {
54
- const parsedTtl = parseDuration(cfg.ttl);
55
- if ("error" in parsedTtl) issues.push(`ttl: ${parsedTtl.error}`);
56
- else ttlSeconds = parsedTtl.seconds;
57
- }
58
- if (issues.length > 0) throw new ConfigValidationError(issues);
59
- const resolved = {
60
- authEnabled: isServiceEnabled(cfg.auth),
61
- dataApiEnabled: isServiceEnabled(cfg.dataApi)
62
- };
63
- if (cfg.parent !== void 0) resolved.parent = cfg.parent;
64
- if (ttlSeconds !== void 0) resolved.ttlSeconds = ttlSeconds;
65
- if (cfg.protected !== void 0) resolved.protected = cfg.protected;
66
- if (cfg.postgres) resolved.postgres = { ...cfg.postgres.computeSettings ? { computeSettings: { ...cfg.postgres.computeSettings } } : {} };
67
- if (cfg.preview) resolved.preview = resolvePreviewConfig(cfg.preview);
68
- return resolved;
86
+ return parsed.data;
69
87
  }
70
- function isServiceEnabled(service) {
71
- return service !== void 0 && service.enabled !== false;
88
+ function isServiceEnabled(toggle) {
89
+ if (toggle === void 0) return false;
90
+ if (typeof toggle === "boolean") return toggle;
91
+ return toggle.enabled !== false;
72
92
  }
73
93
  /**
74
- * Normalize a {@link PreviewConfig} into a {@link ResolvedPreviewConfig}: apply per-function
75
- * deploy defaults, default each bucket's access level to `private`, and collapse the
76
- * `aiGateway` toggle to a boolean using the same present-and-not-`false` rule as
77
- * `auth` / `dataApi`.
94
+ * Normalize the static {@link PreviewInput} (merged with per-branch function tuning) into a
95
+ * {@link ResolvedPreviewConfig}. Returns `undefined` when the policy declares no `preview`
96
+ * block so the field can be omitted entirely. Function slugs / bucket names come from the
97
+ * record keys.
78
98
  */
79
- function resolvePreviewConfig(preview) {
99
+ function resolvePreviewConfig(preview, tuning) {
100
+ if (!preview) return void 0;
101
+ const fnTuning = tuning.preview?.functions ?? {};
80
102
  return {
81
- functions: (preview.functions ?? []).map(resolveFunctionConfig),
82
- buckets: (preview.buckets ?? []).map((bucket) => ({
83
- name: bucket.name,
84
- access: bucket.access ?? "private"
103
+ functions: Object.entries(preview.functions ?? {}).map(([slug, def]) => resolveFunctionConfig(slug, def, fnTuning[slug] ?? {})),
104
+ buckets: Object.entries(preview.buckets ?? {}).map(([name, def]) => ({
105
+ name,
106
+ access: def.access ?? "private"
85
107
  })),
86
108
  aiGatewayEnabled: isServiceEnabled(preview.aiGateway)
87
109
  };
88
110
  }
89
- function resolveFunctionConfig(fn) {
111
+ function resolveFunctionConfig(slug, def, tuning) {
90
112
  return {
91
- slug: fn.slug,
92
- name: fn.name,
93
- source: fn.source,
94
- env: { ...fn.env ?? {} },
95
- runtime: fn.runtime ?? DEFAULT_FUNCTION_RUNTIME,
96
- memoryMib: fn.memoryMib ?? DEFAULT_FUNCTION_MEMORY_MIB,
97
- ...fn.dev ? { dev: fn.dev } : {}
113
+ slug,
114
+ name: def.name,
115
+ source: def.source,
116
+ env: { ...def.env ?? {} },
117
+ runtime: tuning.runtime ?? DEFAULT_FUNCTION_RUNTIME,
118
+ ...def.dev ? { dev: def.dev } : {}
98
119
  };
99
120
  }
100
121
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import { parseDuration } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport { branchConfigSchema, formatZodIssues } from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tConfig,\n\tFunctionConfig,\n\tPreviewConfig,\n\tResolvedBranchConfig,\n\tResolvedFunctionConfig,\n\tResolvedPreviewConfig,\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;\nconst DEFAULT_FUNCTION_MEMORY_MIB = 512 as const;\n\nconst REGION_PREFIX = /^(aws|azure|gcp)-/;\n\n/**\n * Validate and freeze a Neon Platform branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neondatabase/config/v1\";\n *\n * export default defineConfig((branch) => {\n * if (branch.name === \"main\") {\n * return { protected: true, auth: {} };\n * }\n * return { parent: \"main\", ttl: \"7d\" };\n * });\n * ```\n *\n * The `branch` parameter is a **read-only {@link BranchTarget} descriptor** of the branch\n * this policy invocation is deciding fornot a live branch handle. You don't mutate it\n * (`branch.protected = true` does nothing); you switch on its facts (`branch.name`,\n * `branch.isDefault`, `branch.exists`, …) and **return** the desired {@link BranchConfig}.\n * The same callback runs in two modes: against an existing branch (fields populated from\n * Neon) and during pre-create evaluation (`exists: false`, `id` undefined).\n *\n * Pure function — no I/O, no side effects. The returned policy validates its output every\n * time it is evaluated so errors point at the concrete branch target that triggered them.\n */\nexport function defineConfig<const C extends Config>(input: C): C {\n\tif (typeof input !== \"function\") {\n\t\tthrow new ConfigValidationError([\n\t\t\t\"defineConfig expects a function: `export default defineConfig((branch) => ({ ... }))`.\",\n\t\t\t\"Project-level config has moved to `neonctl link`; neon.ts now describes branch-level policy only.\",\n\t\t]);\n\t}\n\n\treturn Object.freeze(input) as C;\n}\n\n/**\n * Evaluate a branch policy for a specific branch target and return a normalized config.\n */\nexport function resolveConfig(\n\tconfig: Config,\n\tbranch: BranchTarget,\n): ResolvedBranchConfig {\n\tlet raw: unknown;\n\ttry {\n\t\traw = config(Object.freeze({ ...branch }));\n\t} catch (cause) {\n\t\tthrow new ConfigValidationError([\n\t\t\t`Config function threw while evaluating branch \"${branch.name}\".`,\n\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t]);\n\t}\n\n\tconst parsed = branchConfigSchema.safeParse(raw);\n\tif (!parsed.success) {\n\t\tthrow new ConfigValidationError(formatZodIssues(parsed.error));\n\t}\n\n\tconst cfg = parsed.data;\n\tconst issues: string[] = [];\n\tlet ttlSeconds: number | undefined;\n\tif (cfg.ttl !== undefined) {\n\t\tconst parsedTtl = parseDuration(cfg.ttl);\n\t\tif (\"error\" in parsedTtl) {\n\t\t\tissues.push(`ttl: ${parsedTtl.error}`);\n\t\t} else {\n\t\t\tttlSeconds = parsedTtl.seconds;\n\t\t}\n\t}\n\tif (issues.length > 0) {\n\t\tthrow new ConfigValidationError(issues);\n\t}\n\n\tconst resolved: ResolvedBranchConfig = {\n\t\tauthEnabled: isServiceEnabled(cfg.auth),\n\t\tdataApiEnabled: isServiceEnabled(cfg.dataApi),\n\t};\n\tif (cfg.parent !== undefined) resolved.parent = cfg.parent;\n\tif (ttlSeconds !== undefined) resolved.ttlSeconds = ttlSeconds;\n\tif (cfg.protected !== undefined) resolved.protected = cfg.protected;\n\tif (cfg.postgres) {\n\t\tresolved.postgres = {\n\t\t\t...(cfg.postgres.computeSettings\n\t\t\t\t? { computeSettings: { ...cfg.postgres.computeSettings } }\n\t\t\t\t: {}),\n\t\t};\n\t}\n\tif (cfg.preview) {\n\t\tresolved.preview = resolvePreviewConfig(cfg.preview);\n\t}\n\treturn resolved;\n}\n\nfunction isServiceEnabled(service: { enabled?: boolean } | undefined): boolean {\n\treturn service !== undefined && service.enabled !== false;\n}\n\n/**\n * Normalize a {@link PreviewConfig} into a {@link ResolvedPreviewConfig}: apply per-function\n * deploy defaults, default each bucket's access level to `private`, and collapse the\n * `aiGateway` toggle to a boolean using the same present-and-not-`false` rule as\n * `auth` / `dataApi`.\n */\nfunction resolvePreviewConfig(preview: PreviewConfig): ResolvedPreviewConfig {\n\treturn {\n\t\tfunctions: (preview.functions ?? []).map(resolveFunctionConfig),\n\t\tbuckets: (preview.buckets ?? []).map((bucket) => ({\n\t\t\tname: bucket.name,\n\t\t\taccess: bucket.access ?? \"private\",\n\t\t})),\n\t\taiGatewayEnabled: isServiceEnabled(preview.aiGateway),\n\t};\n}\n\nfunction resolveFunctionConfig(fn: FunctionConfig): ResolvedFunctionConfig {\n\treturn {\n\t\tslug: fn.slug,\n\t\tname: fn.name,\n\t\tsource: fn.source,\n\t\tenv: { ...(fn.env ?? {}) },\n\t\truntime: fn.runtime ?? DEFAULT_FUNCTION_RUNTIME,\n\t\tmemoryMib: fn.memoryMib ?? DEFAULT_FUNCTION_MEMORY_MIB,\n\t\t// Passed through untouched (no defaults); only `neon dev` reads it.\n\t\t...(fn.dev ? { dev: fn.dev } : {}),\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":";;;;;AAcA,MAAM,2BAA2B;AACjC,MAAM,8BAA8B;AAEpC,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,aAAqC,OAAa;CACjE,IAAI,OAAO,UAAU,YACpB,MAAM,IAAI,sBAAsB,CAC/B,0FACA,mGACD,CAAC;CAGF,OAAO,OAAO,OAAO,KAAK;AAC3B;;;;AAKA,SAAgB,cACf,QACA,QACuB;CACvB,IAAI;CACJ,IAAI;EACH,MAAM,OAAO,OAAO,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC;CAC1C,SAAS,OAAO;EACf,MAAM,IAAI,sBAAsB,CAC/B,kDAAkD,OAAO,KAAK,KAC7D,OAAiB,WAAW,OAAO,KAAK,CAC1C,CAAC;CACF;CAEA,MAAM,SAAS,mBAAmB,UAAU,GAAG;CAC/C,IAAI,CAAC,OAAO,SACX,MAAM,IAAI,sBAAsB,gBAAgB,OAAO,KAAK,CAAC;CAG9D,MAAM,MAAM,OAAO;CACnB,MAAM,SAAmB,CAAC;CAC1B,IAAI;CACJ,IAAI,IAAI,QAAQ,KAAA,GAAW;EAC1B,MAAM,YAAY,cAAc,IAAI,GAAG;EACvC,IAAI,WAAW,WACd,OAAO,KAAK,QAAQ,UAAU,OAAO;OAErC,aAAa,UAAU;CAEzB;CACA,IAAI,OAAO,SAAS,GACnB,MAAM,IAAI,sBAAsB,MAAM;CAGvC,MAAM,WAAiC;EACtC,aAAa,iBAAiB,IAAI,IAAI;EACtC,gBAAgB,iBAAiB,IAAI,OAAO;CAC7C;CACA,IAAI,IAAI,WAAW,KAAA,GAAW,SAAS,SAAS,IAAI;CACpD,IAAI,eAAe,KAAA,GAAW,SAAS,aAAa;CACpD,IAAI,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY,IAAI;CAC1D,IAAI,IAAI,UACP,SAAS,WAAW,EACnB,GAAI,IAAI,SAAS,kBACd,EAAE,iBAAiB,EAAE,GAAG,IAAI,SAAS,gBAAgB,EAAE,IACvD,CAAC,EACL;CAED,IAAI,IAAI,SACP,SAAS,UAAU,qBAAqB,IAAI,OAAO;CAEpD,OAAO;AACR;AAEA,SAAS,iBAAiB,SAAqD;CAC9E,OAAO,YAAY,KAAA,KAAa,QAAQ,YAAY;AACrD;;;;;;;AAQA,SAAS,qBAAqB,SAA+C;CAC5E,OAAO;EACN,YAAY,QAAQ,aAAa,CAAC,GAAG,IAAI,qBAAqB;EAC9D,UAAU,QAAQ,WAAW,CAAC,GAAG,KAAK,YAAY;GACjD,MAAM,OAAO;GACb,QAAQ,OAAO,UAAU;EAC1B,EAAE;EACF,kBAAkB,iBAAiB,QAAQ,SAAS;CACrD;AACD;AAEA,SAAS,sBAAsB,IAA4C;CAC1E,OAAO;EACN,MAAM,GAAG;EACT,MAAM,GAAG;EACT,QAAQ,GAAG;EACX,KAAK,EAAE,GAAI,GAAG,OAAO,CAAC,EAAG;EACzB,SAAS,GAAG,WAAW;EACvB,WAAW,GAAG,aAAa;EAE3B,GAAI,GAAG,MAAM,EAAE,KAAK,GAAG,IAAI,IAAI,CAAC;CACjC;AACD;;;;;;AAOA,SAAgB,gBAAgB,QAAwB;CACvD,IAAI,cAAc,KAAK,MAAM,GAAG,OAAO;CACvC,OAAO,OAAO;AACf"}
1
+ {"version":3,"file":"define-config.js","names":[],"sources":["../../src/lib/define-config.ts"],"sourcesContent":["import { parseDuration } from \"./duration.js\";\nimport { ConfigValidationError } from \"./errors.js\";\nimport {\n\tbranchTuningSchema,\n\tconfigInputSchema,\n\tformatZodIssues,\n} from \"./schema.js\";\nimport type {\n\tBranchTarget,\n\tBranchTuning,\n\tBranchTuningFn,\n\tConfig,\n\tFunctionDef,\n\tFunctionTuning,\n\tPreviewInput,\n\tResolvedBranchConfig,\n\tResolvedFunctionConfig,\n\tResolvedPreviewConfig,\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 REGION_PREFIX = /^(aws|azure|gcp)-/;\n\n/**\n * Validate and freeze a Neon Platform branch policy.\n *\n * Used at the top of `neon.ts`:\n * ```ts\n * import { defineConfig } from \"@neondatabase/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 existso `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 ServiceToggleInput | undefined = undefined,\n\tconst Preview extends PreviewInput | undefined = undefined,\n>(input: {\n\tauth?: Auth;\n\tdataApi?: DataApi;\n\tpreview?: 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: isServiceEnabled(config.dataApi),\n\t};\n\tif (tuning.parent !== undefined) resolved.parent = tuning.parent;\n\tif (tuning.ttl !== undefined) {\n\t\t// `branchTuningSchema` already validated `ttl` with the same `parseDuration`, so\n\t\t// this only converts the validated value to seconds — it cannot fail here.\n\t\tconst parsedTtl = parseDuration(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);\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/**\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): 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] ?? {}),\n\t);\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): ResolvedFunctionConfig {\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\t// Passed through untouched (no defaults); only `neon dev` reads it.\n\t\t...(def.dev ? { dev: def.dev } : {}),\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,4 +1,4 @@
1
- import { NeonApi } from "./neon-api.js";
1
+ import { DeployFunctionInput, NeonApi } from "./neon-api.js";
2
2
 
3
3
  //#region src/lib/neon-api-real.d.ts
4
4
  interface CreateNeonAuthRestInput {
@@ -58,6 +58,18 @@ declare function previewUnavailableError(err: unknown, featureLabel: string): un
58
58
  declare function createNeonAuthRestInput(input: {
59
59
  databaseName?: string;
60
60
  }): CreateNeonAuthRestInput;
61
+ /**
62
+ * Build the `multipart/form-data` body for a function deployment, matching the public
63
+ * `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
64
+ *
65
+ * - `zip` — the bundle as a binary part (named `bundle.zip`).
66
+ * - `runtime` — the function runtime.
67
+ * - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
68
+ * object part), omitted entirely when there are no env vars.
69
+ *
70
+ * Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
71
+ */
72
+ declare function buildFunctionDeployForm(input: DeployFunctionInput): FormData;
61
73
  /**
62
74
  * Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
63
75
  * body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
@@ -70,5 +82,5 @@ declare function createNeonAuthRestInput(input: {
70
82
  */
71
83
  declare function readJsonBody(res: Response): Promise<unknown>;
72
84
  //#endregion
73
- export { createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
85
+ export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
74
86
  //# sourceMappingURL=neon-api-real.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"neon-api-real.d.ts","names":[],"sources":["../../src/lib/neon-api-real.ts"],"mappings":";;;UAoFU,uBAAA;;EAAA,aAAA,CAAA,EAAA,MAAA;AAeV;AAyCC;AAeD;;;;AAES,iBA1DO,iBAAA,CA0DP,OAAA,EAAA;QACE,EAAA,MAAA;SAAR,CAAA,EAAA,MAAA;EAAO;AAq0BV;AAsBA;AAmCA;AAmBA;EAAkC,aAAA,CAAA,EAAA;IAAM,WAAA,CAAA,EAAA,MAAA;IAAW,cAAA,CAAA,EAAA,MAAA;IAAO,UAAA,CAAA,EAAA,MAAA;;IA/7BtD;UA8BM,WAAA;;;;;;;;;;;;iBAaY,2BACX,QAAQ,YACV,cACN,QAAQ;;;;;;;;;;;;iBAq0BK,2BAAA;;;;;;iBAsBA,uBAAA;iBAmCA,uBAAA;;IAEZ;;;;;;;;;;;iBAiBkB,YAAA,MAAkB,WAAW"}
1
+ {"version":3,"file":"neon-api-real.d.ts","names":[],"sources":["../../src/lib/neon-api-real.ts"],"mappings":";;;UAoFU,uBAAA;;EAAA,aAAA,CAAA,EAAA,MAAA;AAeV;AAyCC;AAeD;;;;AAES,iBA1DO,iBAAA,CA0DP,OAAA,EAAA;QACE,EAAA,MAAA;SAAR,CAAA,EAAA,MAAA;EAAO;AAwzBV;AAsBA;AAmCA;AAoBA;EAAuC,aAAA,CAAA,EAAA;IAAQ,WAAA,CAAA,EAAA,MAAA;IAAsB,cAAA,CAAA,EAAA,MAAA;IAAQ,UAAA,CAAA,EAAA,MAAA;EAwBvD,CAAA;CAAY,CAAA,EA38B9B,OA28B8B;UA76BxB,WAAA,CA66B8B;aAAW,EAAA,MAAA;EAAO,cAAA,EAAA,MAAA;;;;;;;;;;iBAh6BpC,2BACX,QAAQ,YACV,cACN,QAAQ;;;;;;;;;;;;iBAwzBK,2BAAA;;;;;;iBAsBA,uBAAA;iBAmCA,uBAAA;;IAEZ;;;;;;;;;;;;iBAkBY,uBAAA,QAA+B,sBAAsB;;;;;;;;;;;iBAwB/C,YAAA,MAAkB,WAAW"}
@@ -284,17 +284,12 @@ var RealNeonApi = class {
284
284
  return this.request("DELETE", path);
285
285
  }
286
286
  /**
287
- * Upload a built function bundle via `multipart/form-data` to the deploy endpoint.
288
- * Sends the bundle as the `file` field plus the deploy params Neon requires.
287
+ * Upload a built function bundle via `multipart/form-data` to the deploy endpoint
288
+ * (`POST .../functions/{slug}/deployments`). Body shape lives in the pure
289
+ * {@link buildFunctionDeployForm} helper so it can be unit-tested against the spec.
289
290
  */
290
291
  async postMultipart(path, input) {
291
- const form = new FormData();
292
- form.set("file", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
293
- form.set("memory_mib", String(input.memoryMib));
294
- form.set("concurrency", "1");
295
- form.set("runtime", input.runtime);
296
- for (const [key, value] of Object.entries(input.environment)) form.set(`environment[${key}]`, value);
297
- return this.request("POST", path, { body: form });
292
+ return this.request("POST", path, { body: buildFunctionDeployForm(input) });
298
293
  }
299
294
  async request(method, path, init = {}) {
300
295
  const url = `${this.restConfig.baseUrl.replace(/\/+$/, "")}${path}`;
@@ -532,6 +527,24 @@ function createNeonAuthRestInput(input) {
532
527
  };
533
528
  }
534
529
  /**
530
+ * Build the `multipart/form-data` body for a function deployment, matching the public
531
+ * `FunctionDeployRequest` schema (`POST .../functions/{slug}/deployments`):
532
+ *
533
+ * - `zip` — the bundle as a binary part (named `bundle.zip`).
534
+ * - `runtime` — the function runtime.
535
+ * - `environment` — a single JSON-encoded string→string map (multipart can't carry a typed
536
+ * object part), omitted entirely when there are no env vars.
537
+ *
538
+ * Pure (no I/O) so it can be unit-tested against the spec without stubbing `fetch`.
539
+ */
540
+ function buildFunctionDeployForm(input) {
541
+ const form = new FormData();
542
+ form.set("zip", new Blob([input.bundle], { type: "application/zip" }), "bundle.zip");
543
+ form.set("runtime", input.runtime);
544
+ if (Object.keys(input.environment).length > 0) form.set("environment", JSON.stringify(input.environment));
545
+ return form;
546
+ }
547
+ /**
535
548
  * Read a response body as JSON, tolerating non-JSON. Some Neon routes return a plain-text
536
549
  * body (e.g. a 404 `"this route does not exist"` for a Preview feature not enabled in the
537
550
  * project/region). Parsing that with `JSON.parse` used to throw a cryptic
@@ -630,6 +643,6 @@ function defaultsToComputeSettings(defaults) {
630
643
  return Object.keys(out).length > 0 ? out : void 0;
631
644
  }
632
645
  //#endregion
633
- export { createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
646
+ export { buildFunctionDeployForm, createNeonAuthRestInput, createRealNeonApi, isPreviewFeatureUnavailable, previewUnavailableError, readJsonBody, retryOnLocked };
634
647
 
635
648
  //# sourceMappingURL=neon-api-real.js.map