@neondatabase/config 0.2.1 → 0.4.1

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, ComputeUnit, Config, ConflictReport, DurationString, DurationUnit, 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, ComputeUnit, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, DiffOptions, DiffResult, DurationString, DurationUnit, 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 & ServiceToggleInput;
40
+ dataApi?: DataApi & ServiceToggleInput;
41
+ preview?: Preview & PreviewInput;
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;;;;;;;;;;;;;;;;;AAeU;AA4BV;;;;;AAGuB;AA6GvB;;;;;;;iBA3JgB,gCACI,kEACG,kEACA;SAQf,OAAO;YACJ,UAAU;YACV,UAAU;WACX,eAAe;IACrB,OAAO,MAAM,SAAS;;;;;;;;iBA4BV,aAAA,SACP,gBACA,eACN;;;;;;iBA6Ga,eAAA"}
@@ -1,10 +1,9 @@
1
1
  import { ConfigValidationError } from "./errors.js";
2
- import { parseDuration } from "./duration.js";
3
- import { branchConfigSchema, formatZodIssues } from "./schema.js";
2
+ import { parseBranchTtl } from "./duration.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 = parseBranchTtl(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 for — not 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 { parseBranchTtl } 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 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 ServiceToggleInput | 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\tdataApi?: DataApi & ServiceToggleInput;\n\tpreview?: Preview & PreviewInput;\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 `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);\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":";;;;;AAsBA,MAAM,2BAA2B;AAEjC,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCtB,SAAgB,aAId,OAWiC;CAClC,IAAI,OAAO,UAAU,YACpB,MAAM,IAAI,sBAAsB,CAC/B,mJACA,2GACD,CAAC;CAEF,IAAI,UAAU,QAAQ,OAAO,UAAU,UACtC,MAAM,IAAI,sBAAsB,CAC/B,oFACD,CAAC;CAGF,MAAM,SAAS,kBAAkB,UAAU,KAAK;CAChD,IAAI,CAAC,OAAO,SACX,MAAM,IAAI,sBAAsB,gBAAgB,OAAO,KAAK,CAAC;CAG9D,OAAO,OAAO,OAAO,EAAE,GAAG,MAAM,CAAC;AAClC;;;;;;;;AASA,SAAgB,cACf,QACA,QACuB;CACvB,MAAM,SAAS,qBAAqB,OAAO,QAAQ,MAAM;CAEzD,MAAM,WAAiC;EACtC,aAAa,iBAAiB,OAAO,IAAI;EACzC,gBAAgB,iBAAiB,OAAO,OAAO;CAChD;CACA,IAAI,OAAO,WAAW,KAAA,GAAW,SAAS,SAAS,OAAO;CAC1D,IAAI,OAAO,QAAQ,KAAA,GAAW;EAG7B,MAAM,YAAY,eAAe,OAAO,GAAG;EAC3C,IAAI,EAAE,WAAW,YAAY,SAAS,aAAa,UAAU;CAC9D;CACA,IAAI,OAAO,cAAc,KAAA,GAAW,SAAS,YAAY,OAAO;CAChE,IAAI,OAAO,UAAU,iBACpB,SAAS,WAAW,EACnB,iBAAiB,EAAE,GAAG,OAAO,SAAS,gBAAgB,EACvD;CAGD,MAAM,UAAU,qBAAqB,OAAO,SAAS,MAAM;CAC3D,IAAI,SAAS,SAAS,UAAU;CAEhC,OAAO;AACR;;;;;AAMA,SAAS,qBACR,UACA,QACe;CACf,IAAI,CAAC,UAAU,OAAO,CAAC;CACvB,IAAI;CACJ,IAAI;EACH,MAAM,SAAS,OAAO,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC;CAC5C,SAAS,OAAO;EACf,MAAM,IAAI,sBAAsB,CAC/B,gDAAgD,OAAO,KAAK,KAC3D,OAAiB,WAAW,OAAO,KAAK,CAC1C,CAAC;CACF;CACA,MAAM,SAAS,mBAAmB,UAAU,OAAO,CAAC,CAAC;CACrD,IAAI,CAAC,OAAO,SACX,MAAM,IAAI,sBAAsB,gBAAgB,OAAO,KAAK,CAAC;CAE9D,OAAO,OAAO;AACf;AAEA,SAAS,iBAAiB,QAAiD;CAC1E,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,IAAI,OAAO,WAAW,WAAW,OAAO;CACxC,OAAO,OAAO,YAAY;AAC3B;;;;;;;AAQA,SAAS,qBACR,SACA,QACoC;CACpC,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,MAAM,WAAW,OAAO,SAAS,aAAa,CAAC;CAY/C,OAAO;EACN,WAZ2C,OAAO,QAClD,QAAQ,aAAa,CAAC,CACvB,EAAE,KAAK,CAAC,MAAM,SACb,sBAAsB,MAAM,KAAK,SAAS,SAAS,CAAC,CAAC,CAS7C;EACR,SARe,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,EAAE,KACpD,CAAC,MAAM,UAAU;GACjB;GACA,QAAQ,IAAI,UAAU;EACvB,EAIM;EACN,kBAAkB,iBAAiB,QAAQ,SAAS;CACrD;AACD;AAEA,SAAS,sBACR,MACA,KACA,QACyB;CACzB,OAAO;EACN;EACA,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,KAAK,EAAE,GAAI,IAAI,OAAO,CAAC,EAAG;EAC1B,SAAS,OAAO,WAAW;EAE3B,GAAI,IAAI,MAAM,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC;CACnC;AACD;;;;;;AAOA,SAAgB,gBAAgB,QAAwB;CACvD,IAAI,cAAc,KAAK,MAAM,GAAG,OAAO;CACvC,OAAO,OAAO;AACf"}
@@ -1,11 +1,18 @@
1
+ import { DurationString } from "./types.js";
2
+
1
3
  //#region src/lib/duration.d.ts
4
+
2
5
  /**
3
- * Parse a TTL value into whole seconds.
6
+ * Parse a duration value into whole seconds.
4
7
  *
5
8
  * Accepted formats:
6
- * - a positive finite number → interpreted as seconds (must be an integer)
7
- * - a positive integer string ("3600") seconds
8
- * - `<number><unit>` where unit is one of `s`, `m`, `h`, `d`, `w` (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
9
+ * - a positive finite **number** → interpreted as seconds (must be an integer)
10
+ * - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`
11
+ * (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
12
+ *
13
+ * A **unit is required** on strings: a bare numeric string like `"7"` is rejected — pass a
14
+ * `number` (`7`) for raw seconds instead. This removes the ambiguity where `"7"` silently
15
+ * meant 7 seconds rather than, say, `"7d"`.
9
16
  *
10
17
  * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
11
18
  */
@@ -14,12 +21,27 @@ declare function parseDuration(input: string | number): {
14
21
  } | {
15
22
  error: string;
16
23
  };
24
+ /** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */
25
+ declare const MAX_BRANCH_TTL_SECONDS: number;
26
+ /**
27
+ * Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the
28
+ * shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days
29
+ * ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.
30
+ *
31
+ * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
32
+ */
33
+ declare function parseBranchTtl(input: string | number): {
34
+ seconds: number;
35
+ } | {
36
+ error: string;
37
+ };
17
38
  /**
18
39
  * Render a TTL in seconds back to the canonical "<n><unit>" form. Used for round-trip
19
40
  * serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds
20
- * when no clean unit boundary matches).
41
+ * when no clean unit boundary matches). The output always carries a unit, so it is a valid
42
+ * {@link DurationString}.
21
43
  */
22
- declare function formatDurationSeconds(totalSeconds: number): string;
44
+ declare function formatDurationSeconds(totalSeconds: number): DurationString;
23
45
  /**
24
46
  * Parse a suspend timeout value into seconds for the Neon API.
25
47
  *
@@ -40,7 +62,7 @@ declare function parseSuspendTimeout(input: false | string | number | undefined)
40
62
  * Format a suspend timeout value from API seconds back to the user-facing type.
41
63
  * Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.
42
64
  */
43
- declare function formatSuspendTimeout(seconds: number): false | string | undefined;
65
+ declare function formatSuspendTimeout(seconds: number): false | DurationString | undefined;
44
66
  //#endregion
45
- export { formatDurationSeconds, formatSuspendTimeout, parseDuration, parseSuspendTimeout };
67
+ export { MAX_BRANCH_TTL_SECONDS, formatDurationSeconds, formatSuspendTimeout, parseBranchTtl, parseDuration, parseSuspendTimeout };
46
68
  //# sourceMappingURL=duration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duration.d.ts","names":[],"sources":["../../src/lib/duration.ts"],"mappings":";;AAUA;AAoDA;AA+BA;AA+CA;;;;;;iBAlIgB,aAAA;;;;;;;;;;iBAoDA,qBAAA;;;;;;;;;;;;iBA+BA,mBAAA;;;;;;;;;iBA+CA,oBAAA"}
1
+ {"version":3,"file":"duration.d.ts","names":[],"sources":["../../src/lib/duration.ts"],"mappings":";;;;;;AAgBA;AAkDA;AASA;AAmBA;AA+BA;AA+CA;;;;;;;iBA5JgB,aAAA;;;;;;cAkDH;;;;;;;;iBASG,cAAA;;;;;;;;;;;iBAmBA,qBAAA,wBAA6C;;;;;;;;;;;;iBA+B7C,mBAAA;;;;;;;;;iBA+CA,oBAAA,2BAEL"}
@@ -1,11 +1,15 @@
1
1
  //#region src/lib/duration.ts
2
2
  /**
3
- * Parse a TTL value into whole seconds.
3
+ * Parse a duration value into whole seconds.
4
4
  *
5
5
  * Accepted formats:
6
- * - a positive finite number → interpreted as seconds (must be an integer)
7
- * - a positive integer string ("3600") seconds
8
- * - `<number><unit>` where unit is one of `s`, `m`, `h`, `d`, `w` (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
6
+ * - a positive finite **number** → interpreted as seconds (must be an integer)
7
+ * - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`
8
+ * (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)
9
+ *
10
+ * A **unit is required** on strings: a bare numeric string like `"7"` is rejected — pass a
11
+ * `number` (`7`) for raw seconds instead. This removes the ambiguity where `"7"` silently
12
+ * meant 7 seconds rather than, say, `"7d"`.
9
13
  *
10
14
  * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
11
15
  */
@@ -18,14 +22,9 @@ function parseDuration(input) {
18
22
  }
19
23
  const trimmed = input.trim();
20
24
  if (trimmed === "") return { error: "duration string is empty" };
21
- const numericMatch = /^(\d+)$/.exec(trimmed);
22
- if (numericMatch) {
23
- const n = Number(numericMatch[1]);
24
- if (n <= 0) return { error: `must be > 0, got "${trimmed}"` };
25
- return { seconds: n };
26
- }
25
+ if (/^\d+$/.test(trimmed)) return { error: `duration string "${input}" is missing a unit; add one of s, m, h, d, w (e.g. "${trimmed}d") or pass ${trimmed} as a number for seconds` };
27
26
  const unitMatch = /^(\d+)([smhdw])$/i.exec(trimmed);
28
- if (!unitMatch) return { error: `invalid duration "${input}"; expected a number followed by one of: s, m, h, d, w (e.g. "30s", "1h", "7d")` };
27
+ if (!unitMatch) return { error: `invalid duration "${input}"; expected an integer followed by one of: s, m, h, d, w (e.g. "30s", "1h", "7d")` };
29
28
  const value = Number(unitMatch[1]);
30
29
  const unit = unitMatch[2].toLowerCase();
31
30
  if (value <= 0) return { error: `must be > 0, got "${trimmed}"` };
@@ -38,10 +37,26 @@ const UNIT_SECONDS = {
38
37
  d: 1440 * 60,
39
38
  w: 10080 * 60
40
39
  };
40
+ /** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */
41
+ const MAX_BRANCH_TTL_SECONDS = 30 * UNIT_SECONDS.d;
42
+ /**
43
+ * Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the
44
+ * shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days
45
+ * ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.
46
+ *
47
+ * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.
48
+ */
49
+ function parseBranchTtl(input) {
50
+ const result = parseDuration(input);
51
+ if ("error" in result) return result;
52
+ if (result.seconds > MAX_BRANCH_TTL_SECONDS) return { error: `branch TTL must be at most 30 days (${MAX_BRANCH_TTL_SECONDS}s), got ${result.seconds}s` };
53
+ return result;
54
+ }
41
55
  /**
42
56
  * Render a TTL in seconds back to the canonical "<n><unit>" form. Used for round-trip
43
57
  * serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds
44
- * when no clean unit boundary matches).
58
+ * when no clean unit boundary matches). The output always carries a unit, so it is a valid
59
+ * {@link DurationString}.
45
60
  */
46
61
  function formatDurationSeconds(totalSeconds) {
47
62
  if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) throw new RangeError(`formatDurationSeconds expected a positive finite number, got ${totalSeconds}`);
@@ -91,6 +106,6 @@ function formatSuspendTimeout(seconds) {
91
106
  return formatDurationSeconds(seconds);
92
107
  }
93
108
  //#endregion
94
- export { formatDurationSeconds, formatSuspendTimeout, parseDuration, parseSuspendTimeout };
109
+ export { MAX_BRANCH_TTL_SECONDS, formatDurationSeconds, formatSuspendTimeout, parseBranchTtl, parseDuration, parseSuspendTimeout };
95
110
 
96
111
  //# sourceMappingURL=duration.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"duration.js","names":[],"sources":["../../src/lib/duration.ts"],"sourcesContent":["/**\n * Parse a TTL value into whole seconds.\n *\n * Accepted formats:\n * - a positive finite number → interpreted as seconds (must be an integer)\n * - a positive integer string (\"3600\") seconds\n * - `<number><unit>` where unit is one of `s`, `m`, `h`, `d`, `w` (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseDuration(\n\tinput: string | number,\n): { seconds: number } | { error: string } {\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn {\n\t\t\t\terror: `must be an integer when passed as number: ${input}`,\n\t\t\t};\n\t\tif (input <= 0) return { error: `must be > 0, got ${input}` };\n\t\treturn { seconds: input };\n\t}\n\n\tconst trimmed = input.trim();\n\tif (trimmed === \"\") return { error: \"duration string is empty\" };\n\n\tconst numericMatch = /^(\\d+)$/.exec(trimmed);\n\tif (numericMatch) {\n\t\tconst n = Number(numericMatch[1]);\n\t\tif (n <= 0) return { error: `must be > 0, got \"${trimmed}\"` };\n\t\treturn { seconds: n };\n\t}\n\n\tconst unitMatch = /^(\\d+)([smhdw])$/i.exec(trimmed);\n\tif (!unitMatch) {\n\t\treturn {\n\t\t\terror: `invalid duration \"${input}\"; expected a number followed by one of: s, m, h, d, w (e.g. \"30s\", \"1h\", \"7d\")`,\n\t\t};\n\t}\n\n\tconst value = Number(unitMatch[1]);\n\tconst unit = unitMatch[2].toLowerCase() as \"s\" | \"m\" | \"h\" | \"d\" | \"w\";\n\tif (value <= 0) return { error: `must be > 0, got \"${trimmed}\"` };\n\n\tconst seconds = value * UNIT_SECONDS[unit];\n\treturn { seconds };\n}\n\nconst UNIT_SECONDS = {\n\ts: 1,\n\tm: 60,\n\th: 60 * 60,\n\td: 24 * 60 * 60,\n\tw: 7 * 24 * 60 * 60,\n} as const;\n\n/**\n * Render a TTL in seconds back to the canonical \"<n><unit>\" form. Used for round-trip\n * serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds\n * when no clean unit boundary matches).\n */\nexport function formatDurationSeconds(totalSeconds: number): string {\n\tif (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {\n\t\tthrow new RangeError(\n\t\t\t`formatDurationSeconds expected a positive finite number, got ${totalSeconds}`,\n\t\t);\n\t}\n\tconst candidates = [\n\t\t[\"w\", UNIT_SECONDS.w],\n\t\t[\"d\", UNIT_SECONDS.d],\n\t\t[\"h\", UNIT_SECONDS.h],\n\t\t[\"m\", UNIT_SECONDS.m],\n\t] as const;\n\tfor (const [unit, perUnit] of candidates) {\n\t\tif (totalSeconds % perUnit === 0) {\n\t\t\treturn `${totalSeconds / perUnit}${unit}`;\n\t\t}\n\t}\n\treturn `${totalSeconds}s`;\n}\n\n/**\n * Parse a suspend timeout value into seconds for the Neon API.\n *\n * Accepted formats:\n * - `false` → -1 (never suspend)\n * - `undefined` → 0 (use platform default)\n * - duration string → parsed seconds (\"5m\", \"1h\", \"7d\")\n * - number → validated seconds (must be 60-604800 or -1/0)\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseSuspendTimeout(\n\tinput: false | string | number | undefined,\n): { seconds: number } | { error: string } {\n\t// false means \"never suspend\"\n\tif (input === false) return { seconds: -1 };\n\n\t// undefined means \"use platform default\"\n\tif (input === undefined) return { seconds: 0 };\n\n\t// If it's a number, validate the range\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn { error: `must be an integer: ${input}` };\n\n\t\t// Allow special values: -1 (never), 0 (default)\n\t\tif (input === -1 || input === 0) return { seconds: input };\n\n\t\t// Validate range for custom timeout: 60s (1 min) to 604800s (1 week)\n\t\tif (input < 60 || input > 604_800) {\n\t\t\treturn {\n\t\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), got ${input}`,\n\t\t\t};\n\t\t}\n\t\treturn { seconds: input };\n\t}\n\n\t// Parse duration string\n\tconst result = parseDuration(input);\n\tif (\"error\" in result) return result;\n\n\t// Validate the parsed duration is in the valid range\n\tconst { seconds } = result;\n\tif (seconds < 60 || seconds > 604_800) {\n\t\treturn {\n\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), \"${input}\" = ${seconds}s`,\n\t\t};\n\t}\n\n\treturn { seconds };\n}\n\n/**\n * Format a suspend timeout value from API seconds back to the user-facing type.\n * Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.\n */\nexport function formatSuspendTimeout(\n\tseconds: number,\n): false | string | undefined {\n\tif (seconds === -1) return false; // never suspend\n\tif (seconds === 0) return undefined; // platform default\n\treturn formatDurationSeconds(seconds);\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,cACf,OAC0C;CAC1C,IAAI,OAAO,UAAU,UAAU;EAC9B,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,OAAO,EAAE,OAAO,wBAAwB,QAAQ;EACjD,IAAI,CAAC,OAAO,UAAU,KAAK,GAC1B,OAAO,EACN,OAAO,6CAA6C,QACrD;EACD,IAAI,SAAS,GAAG,OAAO,EAAE,OAAO,oBAAoB,QAAQ;EAC5D,OAAO,EAAE,SAAS,MAAM;CACzB;CAEA,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,YAAY,IAAI,OAAO,EAAE,OAAO,2BAA2B;CAE/D,MAAM,eAAe,UAAU,KAAK,OAAO;CAC3C,IAAI,cAAc;EACjB,MAAM,IAAI,OAAO,aAAa,EAAE;EAChC,IAAI,KAAK,GAAG,OAAO,EAAE,OAAO,qBAAqB,QAAQ,GAAG;EAC5D,OAAO,EAAE,SAAS,EAAE;CACrB;CAEA,MAAM,YAAY,oBAAoB,KAAK,OAAO;CAClD,IAAI,CAAC,WACJ,OAAO,EACN,OAAO,qBAAqB,MAAM,iFACnC;CAGD,MAAM,QAAQ,OAAO,UAAU,EAAE;CACjC,MAAM,OAAO,UAAU,GAAG,YAAY;CACtC,IAAI,SAAS,GAAG,OAAO,EAAE,OAAO,qBAAqB,QAAQ,GAAG;CAGhE,OAAO,EAAE,SADO,QAAQ,aAAa,MACpB;AAClB;AAEA,MAAM,eAAe;CACpB,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG,OAAU;CACb,GAAG,QAAc;AAClB;;;;;;AAOA,SAAgB,sBAAsB,cAA8B;CACnE,IAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GACrD,MAAM,IAAI,WACT,gEAAgE,cACjE;CAED,MAAM,aAAa;EAClB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;CACrB;CACA,KAAK,MAAM,CAAC,MAAM,YAAY,YAC7B,IAAI,eAAe,YAAY,GAC9B,OAAO,GAAG,eAAe,UAAU;CAGrC,OAAO,GAAG,aAAa;AACxB;;;;;;;;;;;;AAaA,SAAgB,oBACf,OAC0C;CAE1C,IAAI,UAAU,OAAO,OAAO,EAAE,SAAS,GAAG;CAG1C,IAAI,UAAU,KAAA,GAAW,OAAO,EAAE,SAAS,EAAE;CAG7C,IAAI,OAAO,UAAU,UAAU;EAC9B,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,OAAO,EAAE,OAAO,wBAAwB,QAAQ;EACjD,IAAI,CAAC,OAAO,UAAU,KAAK,GAC1B,OAAO,EAAE,OAAO,uBAAuB,QAAQ;EAGhD,IAAI,UAAU,MAAM,UAAU,GAAG,OAAO,EAAE,SAAS,MAAM;EAGzD,IAAI,QAAQ,MAAM,QAAQ,QACzB,OAAO,EACN,OAAO,mFAAmF,QAC3F;EAED,OAAO,EAAE,SAAS,MAAM;CACzB;CAGA,MAAM,SAAS,cAAc,KAAK;CAClC,IAAI,WAAW,QAAQ,OAAO;CAG9B,MAAM,EAAE,YAAY;CACpB,IAAI,UAAU,MAAM,UAAU,QAC7B,OAAO,EACN,OAAO,gFAAgF,MAAM,MAAM,QAAQ,GAC5G;CAGD,OAAO,EAAE,QAAQ;AAClB;;;;;AAMA,SAAgB,qBACf,SAC6B;CAC7B,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,YAAY,GAAG,OAAO,KAAA;CAC1B,OAAO,sBAAsB,OAAO;AACrC"}
1
+ {"version":3,"file":"duration.js","names":[],"sources":["../../src/lib/duration.ts"],"sourcesContent":["import type { DurationString } from \"./types.js\";\n\n/**\n * Parse a duration value into whole seconds.\n *\n * Accepted formats:\n * - a positive finite **number** → interpreted as seconds (must be an integer)\n * - a **string** of the form `<integer><unit>` where unit is one of `s`, `m`, `h`, `d`, `w`\n * (e.g. `30s`, `5m`, `1h`, `7d`, `2w`)\n *\n * A **unit is required** on strings: a bare numeric string like `\"7\"` is rejected — pass a\n * `number` (`7`) for raw seconds instead. This removes the ambiguity where `\"7\"` silently\n * meant 7 seconds rather than, say, `\"7d\"`.\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseDuration(\n\tinput: string | number,\n): { seconds: number } | { error: string } {\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn {\n\t\t\t\terror: `must be an integer when passed as number: ${input}`,\n\t\t\t};\n\t\tif (input <= 0) return { error: `must be > 0, got ${input}` };\n\t\treturn { seconds: input };\n\t}\n\n\tconst trimmed = input.trim();\n\tif (trimmed === \"\") return { error: \"duration string is empty\" };\n\n\t// A bare numeric string is rejected on purpose: pass a number for raw seconds, or add a\n\t// unit (e.g. \"7d\"). Detected explicitly so we can give a targeted hint instead of the\n\t// generic \"invalid duration\" message.\n\tif (/^\\d+$/.test(trimmed)) {\n\t\treturn {\n\t\t\terror: `duration string \"${input}\" is missing a unit; add one of s, m, h, d, w (e.g. \"${trimmed}d\") or pass ${trimmed} as a number for seconds`,\n\t\t};\n\t}\n\n\tconst unitMatch = /^(\\d+)([smhdw])$/i.exec(trimmed);\n\tif (!unitMatch) {\n\t\treturn {\n\t\t\terror: `invalid duration \"${input}\"; expected an integer followed by one of: s, m, h, d, w (e.g. \"30s\", \"1h\", \"7d\")`,\n\t\t};\n\t}\n\n\tconst value = Number(unitMatch[1]);\n\tconst unit = unitMatch[2].toLowerCase() as \"s\" | \"m\" | \"h\" | \"d\" | \"w\";\n\tif (value <= 0) return { error: `must be > 0, got \"${trimmed}\"` };\n\n\tconst seconds = value * UNIT_SECONDS[unit];\n\treturn { seconds };\n}\n\nconst UNIT_SECONDS = {\n\ts: 1,\n\tm: 60,\n\th: 60 * 60,\n\td: 24 * 60 * 60,\n\tw: 7 * 24 * 60 * 60,\n} as const;\n\n/** Neon's branch-expiration ceiling: the API rejects an `expires_at` more than 30 days out. */\nexport const MAX_BRANCH_TTL_SECONDS = 30 * UNIT_SECONDS.d;\n\n/**\n * Parse a branch TTL into seconds, enforcing Neon's branch-expiration limit on top of the\n * shared {@link parseDuration} rules: the result must be `> 0` and at most 30 days\n * ({@link MAX_BRANCH_TTL_SECONDS}), since the API caps `expires_at` at 30 days from now.\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseBranchTtl(\n\tinput: string | number,\n): { seconds: number } | { error: string } {\n\tconst result = parseDuration(input);\n\tif (\"error\" in result) return result;\n\tif (result.seconds > MAX_BRANCH_TTL_SECONDS) {\n\t\treturn {\n\t\t\terror: `branch TTL must be at most 30 days (${MAX_BRANCH_TTL_SECONDS}s), got ${result.seconds}s`,\n\t\t};\n\t}\n\treturn result;\n}\n\n/**\n * Render a TTL in seconds back to the canonical \"<n><unit>\" form. Used for round-trip\n * serialization when {@link pullConfig} emits a TTL value (it always falls back to seconds\n * when no clean unit boundary matches). The output always carries a unit, so it is a valid\n * {@link DurationString}.\n */\nexport function formatDurationSeconds(totalSeconds: number): DurationString {\n\tif (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {\n\t\tthrow new RangeError(\n\t\t\t`formatDurationSeconds expected a positive finite number, got ${totalSeconds}`,\n\t\t);\n\t}\n\tconst candidates = [\n\t\t[\"w\", UNIT_SECONDS.w],\n\t\t[\"d\", UNIT_SECONDS.d],\n\t\t[\"h\", UNIT_SECONDS.h],\n\t\t[\"m\", UNIT_SECONDS.m],\n\t] as const;\n\tfor (const [unit, perUnit] of candidates) {\n\t\tif (totalSeconds % perUnit === 0) {\n\t\t\treturn `${totalSeconds / perUnit}${unit}`;\n\t\t}\n\t}\n\treturn `${totalSeconds}s`;\n}\n\n/**\n * Parse a suspend timeout value into seconds for the Neon API.\n *\n * Accepted formats:\n * - `false` → -1 (never suspend)\n * - `undefined` → 0 (use platform default)\n * - duration string → parsed seconds (\"5m\", \"1h\", \"7d\")\n * - number → validated seconds (must be 60-604800 or -1/0)\n *\n * Returns `{ seconds }` on success or `{ error }` on failure. Pure function — never throws.\n */\nexport function parseSuspendTimeout(\n\tinput: false | string | number | undefined,\n): { seconds: number } | { error: string } {\n\t// false means \"never suspend\"\n\tif (input === false) return { seconds: -1 };\n\n\t// undefined means \"use platform default\"\n\tif (input === undefined) return { seconds: 0 };\n\n\t// If it's a number, validate the range\n\tif (typeof input === \"number\") {\n\t\tif (!Number.isFinite(input))\n\t\t\treturn { error: `not a finite number: ${input}` };\n\t\tif (!Number.isInteger(input))\n\t\t\treturn { error: `must be an integer: ${input}` };\n\n\t\t// Allow special values: -1 (never), 0 (default)\n\t\tif (input === -1 || input === 0) return { seconds: input };\n\n\t\t// Validate range for custom timeout: 60s (1 min) to 604800s (1 week)\n\t\tif (input < 60 || input > 604_800) {\n\t\t\treturn {\n\t\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), got ${input}`,\n\t\t\t};\n\t\t}\n\t\treturn { seconds: input };\n\t}\n\n\t// Parse duration string\n\tconst result = parseDuration(input);\n\tif (\"error\" in result) return result;\n\n\t// Validate the parsed duration is in the valid range\n\tconst { seconds } = result;\n\tif (seconds < 60 || seconds > 604_800) {\n\t\treturn {\n\t\t\terror: `suspend timeout must be between 60 and 604800 seconds (1 minute to 1 week), \"${input}\" = ${seconds}s`,\n\t\t};\n\t}\n\n\treturn { seconds };\n}\n\n/**\n * Format a suspend timeout value from API seconds back to the user-facing type.\n * Returns `false` for -1 (never suspend), `undefined` for 0 (default), or a duration string.\n */\nexport function formatSuspendTimeout(\n\tseconds: number,\n): false | DurationString | undefined {\n\tif (seconds === -1) return false; // never suspend\n\tif (seconds === 0) return undefined; // platform default\n\treturn formatDurationSeconds(seconds);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,cACf,OAC0C;CAC1C,IAAI,OAAO,UAAU,UAAU;EAC9B,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,OAAO,EAAE,OAAO,wBAAwB,QAAQ;EACjD,IAAI,CAAC,OAAO,UAAU,KAAK,GAC1B,OAAO,EACN,OAAO,6CAA6C,QACrD;EACD,IAAI,SAAS,GAAG,OAAO,EAAE,OAAO,oBAAoB,QAAQ;EAC5D,OAAO,EAAE,SAAS,MAAM;CACzB;CAEA,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,YAAY,IAAI,OAAO,EAAE,OAAO,2BAA2B;CAK/D,IAAI,QAAQ,KAAK,OAAO,GACvB,OAAO,EACN,OAAO,oBAAoB,MAAM,uDAAuD,QAAQ,cAAc,QAAQ,0BACvH;CAGD,MAAM,YAAY,oBAAoB,KAAK,OAAO;CAClD,IAAI,CAAC,WACJ,OAAO,EACN,OAAO,qBAAqB,MAAM,mFACnC;CAGD,MAAM,QAAQ,OAAO,UAAU,EAAE;CACjC,MAAM,OAAO,UAAU,GAAG,YAAY;CACtC,IAAI,SAAS,GAAG,OAAO,EAAE,OAAO,qBAAqB,QAAQ,GAAG;CAGhE,OAAO,EAAE,SADO,QAAQ,aAAa,MACpB;AAClB;AAEA,MAAM,eAAe;CACpB,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG,OAAU;CACb,GAAG,QAAc;AAClB;;AAGA,MAAa,yBAAyB,KAAK,aAAa;;;;;;;;AASxD,SAAgB,eACf,OAC0C;CAC1C,MAAM,SAAS,cAAc,KAAK;CAClC,IAAI,WAAW,QAAQ,OAAO;CAC9B,IAAI,OAAO,UAAU,wBACpB,OAAO,EACN,OAAO,uCAAuC,uBAAuB,UAAU,OAAO,QAAQ,GAC/F;CAED,OAAO;AACR;;;;;;;AAQA,SAAgB,sBAAsB,cAAsC;CAC3E,IAAI,CAAC,OAAO,SAAS,YAAY,KAAK,gBAAgB,GACrD,MAAM,IAAI,WACT,gEAAgE,cACjE;CAED,MAAM,aAAa;EAClB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;EACpB,CAAC,KAAK,aAAa,CAAC;CACrB;CACA,KAAK,MAAM,CAAC,MAAM,YAAY,YAC7B,IAAI,eAAe,YAAY,GAC9B,OAAO,GAAG,eAAe,UAAU;CAGrC,OAAO,GAAG,aAAa;AACxB;;;;;;;;;;;;AAaA,SAAgB,oBACf,OAC0C;CAE1C,IAAI,UAAU,OAAO,OAAO,EAAE,SAAS,GAAG;CAG1C,IAAI,UAAU,KAAA,GAAW,OAAO,EAAE,SAAS,EAAE;CAG7C,IAAI,OAAO,UAAU,UAAU;EAC9B,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,OAAO,EAAE,OAAO,wBAAwB,QAAQ;EACjD,IAAI,CAAC,OAAO,UAAU,KAAK,GAC1B,OAAO,EAAE,OAAO,uBAAuB,QAAQ;EAGhD,IAAI,UAAU,MAAM,UAAU,GAAG,OAAO,EAAE,SAAS,MAAM;EAGzD,IAAI,QAAQ,MAAM,QAAQ,QACzB,OAAO,EACN,OAAO,mFAAmF,QAC3F;EAED,OAAO,EAAE,SAAS,MAAM;CACzB;CAGA,MAAM,SAAS,cAAc,KAAK;CAClC,IAAI,WAAW,QAAQ,OAAO;CAG9B,MAAM,EAAE,YAAY;CACpB,IAAI,UAAU,MAAM,UAAU,QAC7B,OAAO,EACN,OAAO,gFAAgF,MAAM,MAAM,QAAQ,GAC5G;CAGD,OAAO,EAAE,QAAQ;AAClB;;;;;AAMA,SAAgB,qBACf,SACqC;CACrC,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,YAAY,GAAG,OAAO,KAAA;CAC1B,OAAO,sBAAsB,OAAO;AACrC"}
@@ -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"}