@neondatabase/config 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE.md +178 -0
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +8 -0
  4. package/dist/lib/auth.d.ts +63 -0
  5. package/dist/lib/auth.d.ts.map +1 -0
  6. package/dist/lib/auth.js +93 -0
  7. package/dist/lib/auth.js.map +1 -0
  8. package/dist/lib/define-config.d.ts +43 -0
  9. package/dist/lib/define-config.d.ts.map +1 -0
  10. package/dist/lib/define-config.js +111 -0
  11. package/dist/lib/define-config.js.map +1 -0
  12. package/dist/lib/diff.d.ts +109 -0
  13. package/dist/lib/diff.d.ts.map +1 -0
  14. package/dist/lib/diff.js +205 -0
  15. package/dist/lib/diff.js.map +1 -0
  16. package/dist/lib/duration.d.ts +46 -0
  17. package/dist/lib/duration.d.ts.map +1 -0
  18. package/dist/lib/duration.js +96 -0
  19. package/dist/lib/duration.js.map +1 -0
  20. package/dist/lib/errors.d.ts +129 -0
  21. package/dist/lib/errors.d.ts.map +1 -0
  22. package/dist/lib/errors.js +168 -0
  23. package/dist/lib/errors.js.map +1 -0
  24. package/dist/lib/loader.d.ts +44 -0
  25. package/dist/lib/loader.d.ts.map +1 -0
  26. package/dist/lib/loader.js +119 -0
  27. package/dist/lib/loader.js.map +1 -0
  28. package/dist/lib/neon-api-real.d.ts +45 -0
  29. package/dist/lib/neon-api-real.d.ts.map +1 -0
  30. package/dist/lib/neon-api-real.js +582 -0
  31. package/dist/lib/neon-api-real.js.map +1 -0
  32. package/dist/lib/neon-api.d.ts +262 -0
  33. package/dist/lib/neon-api.d.ts.map +1 -0
  34. package/dist/lib/neon-api.js +1 -0
  35. package/dist/lib/patterns.d.ts +43 -0
  36. package/dist/lib/patterns.d.ts.map +1 -0
  37. package/dist/lib/patterns.js +76 -0
  38. package/dist/lib/patterns.js.map +1 -0
  39. package/dist/lib/schema.d.ts +109 -0
  40. package/dist/lib/schema.d.ts.map +1 -0
  41. package/dist/lib/schema.js +199 -0
  42. package/dist/lib/schema.js.map +1 -0
  43. package/dist/lib/types.d.ts +259 -0
  44. package/dist/lib/types.d.ts.map +1 -0
  45. package/dist/lib/types.js +1 -0
  46. package/dist/lib/wrap-neon-error.d.ts +30 -0
  47. package/dist/lib/wrap-neon-error.d.ts.map +1 -0
  48. package/dist/lib/wrap-neon-error.js +139 -0
  49. package/dist/lib/wrap-neon-error.js.map +1 -0
  50. package/dist/v1.d.ts +132 -0
  51. package/dist/v1.d.ts.map +1 -0
  52. package/dist/v1.js +69 -0
  53. package/dist/v1.js.map +1 -0
  54. package/package.json +67 -17
  55. package/.env.example +0 -5
  56. package/e2e/errors.e2e.test.ts +0 -52
  57. package/e2e/helpers.ts +0 -205
  58. package/e2e/load-env.ts +0 -29
  59. package/e2e/setup.ts +0 -24
  60. package/src/index.ts +0 -5
  61. package/src/lib/auth.test.ts +0 -166
  62. package/src/lib/auth.ts +0 -124
  63. package/src/lib/define-config.test.ts +0 -161
  64. package/src/lib/define-config.ts +0 -152
  65. package/src/lib/diff.test.ts +0 -142
  66. package/src/lib/diff.ts +0 -391
  67. package/src/lib/duration.test.ts +0 -105
  68. package/src/lib/duration.ts +0 -147
  69. package/src/lib/errors.test.ts +0 -26
  70. package/src/lib/errors.ts +0 -220
  71. package/src/lib/fake-neon-api.ts +0 -782
  72. package/src/lib/loader.test.ts +0 -35
  73. package/src/lib/loader.ts +0 -215
  74. package/src/lib/neon-api-real.test.ts +0 -72
  75. package/src/lib/neon-api-real.ts +0 -1123
  76. package/src/lib/neon-api.ts +0 -356
  77. package/src/lib/patterns.test.ts +0 -80
  78. package/src/lib/patterns.ts +0 -98
  79. package/src/lib/schema.test.ts +0 -88
  80. package/src/lib/schema.ts +0 -252
  81. package/src/lib/test-utils.ts +0 -83
  82. package/src/lib/types.ts +0 -268
  83. package/src/lib/wrap-neon-error.test.ts +0 -145
  84. package/src/lib/wrap-neon-error.ts +0 -204
  85. package/src/v1.test.ts +0 -33
  86. package/src/v1.ts +0 -148
  87. package/tsconfig.json +0 -4
  88. package/tsdown.config.ts +0 -19
  89. package/vitest.config.ts +0 -19
  90. package/vitest.e2e.config.ts +0 -29
@@ -1,166 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
- import { readNeonctlCredentials, resolveApiKey } from "./auth.js";
3
- import { makeTempRepo, stubCleanNeonEnv } from "./test-utils.js";
4
-
5
- const cleanups: Array<() => void> = [];
6
- afterEach(() => {
7
- while (cleanups.length > 0) cleanups.shift()?.();
8
- });
9
-
10
- beforeEach(() => {
11
- stubCleanNeonEnv();
12
- });
13
-
14
- function setupHome(files: Record<string, string | null>): string {
15
- const repo = makeTempRepo(files);
16
- cleanups.push(repo.cleanup);
17
- return repo.root;
18
- }
19
-
20
- describe("readNeonctlCredentials", () => {
21
- test("reads access_token from <home>/.config/neonctl/credentials.json by default", () => {
22
- const home = setupHome({
23
- ".config/neonctl/credentials.json": JSON.stringify({
24
- access_token: "oauth-token-abc",
25
- refresh_token: "rt-xyz",
26
- }),
27
- });
28
- vi.stubEnv("HOME", home);
29
- const creds = readNeonctlCredentials();
30
- expect(creds?.access_token).toBe("oauth-token-abc");
31
- expect(creds?.refresh_token).toBe("rt-xyz");
32
- });
33
-
34
- test("honours NEONCTL_CONFIG_DIR over the default location", () => {
35
- const home = setupHome({
36
- ".config/neonctl/credentials.json": JSON.stringify({
37
- access_token: "default-loc",
38
- }),
39
- "custom/credentials.json": JSON.stringify({
40
- access_token: "from-env-dir",
41
- }),
42
- });
43
- vi.stubEnv("HOME", home);
44
- vi.stubEnv("NEONCTL_CONFIG_DIR", `${home}/custom`);
45
- const creds = readNeonctlCredentials();
46
- expect(creds?.access_token).toBe("from-env-dir");
47
- });
48
-
49
- test("honours explicit configDir over the env var", () => {
50
- const home = setupHome({
51
- ".config/neonctl/credentials.json": JSON.stringify({
52
- access_token: "default-loc",
53
- }),
54
- "env/credentials.json": JSON.stringify({
55
- access_token: "from-env-dir",
56
- }),
57
- "opt/credentials.json": JSON.stringify({
58
- access_token: "from-option",
59
- }),
60
- });
61
- vi.stubEnv("HOME", home);
62
- vi.stubEnv("NEONCTL_CONFIG_DIR", `${home}/env`);
63
- const creds = readNeonctlCredentials({ configDir: `${home}/opt` });
64
- expect(creds?.access_token).toBe("from-option");
65
- });
66
-
67
- test("returns null when the file is missing", () => {
68
- const home = setupHome({ ".config/neonctl/.keep": "" });
69
- vi.stubEnv("HOME", home);
70
- expect(readNeonctlCredentials()).toBeNull();
71
- });
72
-
73
- test("returns null on malformed JSON instead of throwing", () => {
74
- const home = setupHome({
75
- ".config/neonctl/credentials.json": "not json",
76
- });
77
- vi.stubEnv("HOME", home);
78
- expect(readNeonctlCredentials()).toBeNull();
79
- });
80
-
81
- test("returns null when access_token is missing or empty", () => {
82
- const home = setupHome({
83
- ".config/neonctl/credentials.json": JSON.stringify({
84
- refresh_token: "rt-only",
85
- }),
86
- });
87
- vi.stubEnv("HOME", home);
88
- expect(readNeonctlCredentials()).toBeNull();
89
- const home2 = setupHome({
90
- ".config/neonctl/credentials.json": JSON.stringify({
91
- access_token: "",
92
- }),
93
- });
94
- vi.stubEnv("HOME", home2);
95
- expect(readNeonctlCredentials()).toBeNull();
96
- });
97
-
98
- test("returns null when no home dir resolvable", () => {
99
- // `stubCleanNeonEnv()` already cleared HOME and USERPROFILE.
100
- expect(readNeonctlCredentials()).toBeNull();
101
- });
102
-
103
- test("falls back to USERPROFILE on Windows-style env", () => {
104
- const winHome = setupHome({
105
- ".config/neonctl/credentials.json": JSON.stringify({
106
- access_token: "win-token",
107
- }),
108
- });
109
- vi.stubEnv("USERPROFILE", winHome);
110
- const creds = readNeonctlCredentials();
111
- expect(creds?.access_token).toBe("win-token");
112
- });
113
- });
114
-
115
- describe("resolveApiKey — priority chain", () => {
116
- test("explicit option wins over env wins over credentials.json", () => {
117
- const home = setupHome({
118
- ".config/neonctl/credentials.json": JSON.stringify({
119
- access_token: "from-file",
120
- }),
121
- });
122
- vi.stubEnv("HOME", home);
123
- vi.stubEnv("NEON_API_KEY", "from-env");
124
- expect(resolveApiKey({ apiKey: "from-option" })).toEqual({
125
- token: "from-option",
126
- source: "option",
127
- });
128
-
129
- expect(resolveApiKey()).toEqual({ token: "from-env", source: "env" });
130
-
131
- vi.stubEnv("NEON_API_KEY", undefined);
132
- expect(resolveApiKey()).toEqual({
133
- token: "from-file",
134
- source: "neonctl",
135
- });
136
- });
137
-
138
- test("returns null when no source provides a token", () => {
139
- const home = setupHome({ ".config/neonctl/.keep": "" });
140
- vi.stubEnv("HOME", home);
141
- expect(resolveApiKey()).toBeNull();
142
- });
143
-
144
- test("treats whitespace-only option / env as missing", () => {
145
- const home = setupHome({
146
- ".config/neonctl/credentials.json": JSON.stringify({
147
- access_token: "from-file",
148
- }),
149
- });
150
- vi.stubEnv("HOME", home);
151
- vi.stubEnv("NEON_API_KEY", " ");
152
- expect(resolveApiKey({ apiKey: " " })).toEqual({
153
- token: "from-file",
154
- source: "neonctl",
155
- });
156
- });
157
-
158
- test("trims whitespace around the resolved token", () => {
159
- const home = setupHome({ ".config/neonctl/.keep": "" });
160
- vi.stubEnv("HOME", home);
161
- expect(resolveApiKey({ apiKey: " napi_x " })).toEqual({
162
- token: "napi_x",
163
- source: "option",
164
- });
165
- });
166
- });
package/src/lib/auth.ts DELETED
@@ -1,124 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { ErrorCode, PlatformError } from "./errors.js";
4
- import type { NeonApi } from "./neon-api.js";
5
- import { createRealNeonApi } from "./neon-api-real.js";
6
-
7
- /**
8
- * Minimal shape of `~/.config/neonctl/credentials.json` we read. `neonctl` writes more
9
- * fields (refresh_token, expires_at, …) but only `access_token` is what we need — it's a
10
- * Bearer token the Neon API accepts on the same endpoints `napi_*` API keys do.
11
- */
12
- export interface NeonctlCredentials {
13
- access_token: string;
14
- [key: string]: unknown;
15
- }
16
-
17
- /**
18
- * Locate and read the OAuth credentials neonctl writes after `neon auth`.
19
- *
20
- * Resolution:
21
- * 1. `options.configDir` (explicit override — mirrors neonctl's `--config-dir` flag).
22
- * 2. `NEONCTL_CONFIG_DIR` environment variable.
23
- * 3. `<home>/.config/neonctl/credentials.json` (the neonctl default; `home` reads
24
- * `HOME`, falling back to `USERPROFILE` for Windows parity).
25
- *
26
- * Returns `null` (never throws) when the file is missing, unreadable, malformed, or has
27
- * no `access_token` — so callers can use this as a quiet fallback in a resolution chain
28
- * without try/catch noise.
29
- */
30
- export function readNeonctlCredentials(
31
- options: { configDir?: string } = {},
32
- ): NeonctlCredentials | null {
33
- const home = process.env.HOME ?? process.env.USERPROFILE;
34
- const configDir =
35
- options.configDir ??
36
- process.env.NEONCTL_CONFIG_DIR ??
37
- (home ? resolve(home, ".config", "neonctl") : undefined);
38
- if (!configDir) return null;
39
-
40
- const credentialsPath = resolve(configDir, "credentials.json");
41
- if (!existsSync(credentialsPath)) return null;
42
-
43
- let raw: string;
44
- try {
45
- raw = readFileSync(credentialsPath, "utf-8");
46
- } catch {
47
- return null;
48
- }
49
-
50
- let parsed: unknown;
51
- try {
52
- parsed = JSON.parse(raw);
53
- } catch {
54
- return null;
55
- }
56
-
57
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
58
- return null;
59
- const obj = parsed as Record<string, unknown>;
60
- if (typeof obj.access_token !== "string" || obj.access_token === "")
61
- return null;
62
- return obj as NeonctlCredentials;
63
- }
64
-
65
- /**
66
- * Resolution chain for the Bearer token sent to the Neon API. Each entry wins over the
67
- * next:
68
- *
69
- * 1. `options.apiKey` (explicit).
70
- * 2. `NEON_API_KEY` environment variable.
71
- * 3. `access_token` from `~/.config/neonctl/credentials.json` (or `NEONCTL_CONFIG_DIR`).
72
- *
73
- * Returns `null` when no source provides one. Callers wrap the null case in a
74
- * `PLATFORM_MISSING_API_KEY` error with a message tailored to the operation.
75
- */
76
- export function resolveApiKey(
77
- options: { apiKey?: string; configDir?: string } = {},
78
- ): { token: string; source: "option" | "env" | "neonctl" } | null {
79
- if (options.apiKey && options.apiKey.trim() !== "") {
80
- return { token: options.apiKey.trim(), source: "option" };
81
- }
82
- const envKey = process.env.NEON_API_KEY;
83
- if (typeof envKey === "string" && envKey.trim() !== "") {
84
- return { token: envKey.trim(), source: "env" };
85
- }
86
- const creds = readNeonctlCredentials(
87
- options.configDir ? { configDir: options.configDir } : {},
88
- );
89
- if (creds) {
90
- return { token: creds.access_token, source: "neonctl" };
91
- }
92
- return null;
93
- }
94
-
95
- /**
96
- * Resolve the Neon API key via the standard chain (option → `NEON_API_KEY` env →
97
- * `~/.config/neonctl/credentials.json`) and construct a real {@link NeonApi} adapter from
98
- * it, or throw a uniform `PLATFORM_MISSING_API_KEY` error if no key can be found.
99
- *
100
- * Used by `pullConfig`, `pushConfig`, `fetchEnv`, and `branch` to build their default
101
- * `NeonApi` when the caller doesn't inject one. `operation` is the calling function's
102
- * name (e.g. `"pushConfig"`, `"branch"`) — it's prepended to the error message so users
103
- * can tell which call surfaced the missing key.
104
- */
105
- export function createNeonApiFromOptions(
106
- operation: string,
107
- options: {
108
- apiKey?: string;
109
- } = {},
110
- ): NeonApi {
111
- const resolved = resolveApiKey(
112
- options.apiKey ? { apiKey: options.apiKey } : {},
113
- );
114
- if (resolved) return createRealNeonApi({ apiKey: resolved.token });
115
- throw new PlatformError(
116
- ErrorCode.MissingApiKey,
117
- [
118
- `${operation} has no Neon API key to work with.`,
119
- "Tried (in order): `apiKey` option, NEON_API_KEY env, and `~/.config/neonctl/credentials.json`.",
120
- "Either pass `apiKey` directly, set NEON_API_KEY, run `npx neonctl auth` to populate the credentials file, or pass a custom `api` adapter (e.g. an in-memory fake for tests).",
121
- "Generate a key at https://console.neon.tech/app/settings/api-keys.",
122
- ].join(" "),
123
- );
124
- }
@@ -1,161 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { defineConfig, resolveConfig } from "./define-config.js";
3
- import { ConfigValidationError } from "./errors.js";
4
-
5
- describe("defineConfig", () => {
6
- test("accepts a branch policy function and preserves literal behavior", () => {
7
- const config = defineConfig((branch) => {
8
- if (branch.name === "main")
9
- return { protected: true, auth: { enabled: true } };
10
- return { parent: "main", ttl: "7d" };
11
- });
12
- expect(typeof config).toBe("function");
13
- expect(config({ name: "main", exists: true })).toEqual({
14
- protected: true,
15
- auth: { enabled: true },
16
- });
17
- });
18
-
19
- test("rejects object configs so project-level config is not accepted", () => {
20
- expect(() => defineConfig({ project: { name: "x" } } as never)).toThrow(
21
- ConfigValidationError,
22
- );
23
- });
24
- });
25
-
26
- describe("resolveConfig", () => {
27
- test("normalizes branch policy output", () => {
28
- const config = defineConfig(() => ({
29
- parent: "main",
30
- ttl: "1h",
31
- protected: true,
32
- postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
33
- auth: {},
34
- dataApi: {},
35
- }));
36
- const resolved = resolveConfig(config, {
37
- name: "dev-a",
38
- exists: false,
39
- });
40
- expect(resolved).toMatchObject({
41
- parent: "main",
42
- ttlSeconds: 3600,
43
- protected: true,
44
- authEnabled: true,
45
- dataApiEnabled: true,
46
- postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
47
- });
48
- });
49
-
50
- test("treats explicit service false as disabled", () => {
51
- const config = defineConfig(() => ({
52
- auth: { enabled: false },
53
- dataApi: { enabled: false },
54
- }));
55
- const resolved = resolveConfig(config, {
56
- name: "dev-a",
57
- exists: false,
58
- });
59
- expect(resolved.authEnabled).toBe(false);
60
- expect(resolved.dataApiEnabled).toBe(false);
61
- });
62
-
63
- test("reports invalid returned branch config", () => {
64
- const config = defineConfig(() => ({ parent: "preview-*" }));
65
- expect(() =>
66
- resolveConfig(config, { name: "dev", exists: false }),
67
- ).toThrow(ConfigValidationError);
68
- });
69
-
70
- test("resolves preview functions with deploy defaults and buckets with private access", () => {
71
- const config = defineConfig(() => ({
72
- preview: {
73
- functions: [
74
- {
75
- name: "Hello World",
76
- slug: "hello-world",
77
- source: "./functions/hello-world.ts",
78
- env: { RESEND_API_KEY: "re_abc" },
79
- },
80
- ],
81
- buckets: [{ name: "uploads" }],
82
- aiGateway: { enabled: true },
83
- },
84
- }));
85
- const resolved = resolveConfig(config, {
86
- name: "preview-1",
87
- exists: false,
88
- });
89
- expect(resolved.preview).toEqual({
90
- functions: [
91
- {
92
- slug: "hello-world",
93
- name: "Hello World",
94
- source: "./functions/hello-world.ts",
95
- env: { RESEND_API_KEY: "re_abc" },
96
- runtime: "nodejs24",
97
- memoryMib: 512,
98
- },
99
- ],
100
- buckets: [{ name: "uploads", access: "private" }],
101
- aiGatewayEnabled: true,
102
- });
103
- });
104
-
105
- test("treats aiGateway enabled:false as disabled", () => {
106
- const config = defineConfig(() => ({
107
- preview: { aiGateway: { enabled: false } },
108
- }));
109
- const resolved = resolveConfig(config, {
110
- name: "preview-1",
111
- exists: false,
112
- });
113
- expect(resolved.preview?.aiGatewayEnabled).toBe(false);
114
- });
115
-
116
- test("rejects a function env value that is undefined (e.g. unset process.env)", () => {
117
- const config = defineConfig(() => ({
118
- preview: {
119
- functions: [
120
- {
121
- name: "Hello",
122
- slug: "hello",
123
- source: "./hello.ts",
124
- // Simulates `RESEND_API_KEY: process.env.RESEND_API_KEY` when unset.
125
- env: { RESEND_API_KEY: undefined as unknown as string },
126
- },
127
- ],
128
- },
129
- }));
130
- expect(() =>
131
- resolveConfig(config, { name: "preview-1", exists: false }),
132
- ).toThrow(ConfigValidationError);
133
- });
134
-
135
- test("rejects an invalid function slug", () => {
136
- const config = defineConfig(() => ({
137
- preview: {
138
- functions: [
139
- { name: "Bad", slug: "Hello World", source: "./x.ts" },
140
- ],
141
- },
142
- }));
143
- expect(() =>
144
- resolveConfig(config, { name: "preview-1", exists: false }),
145
- ).toThrow(ConfigValidationError);
146
- });
147
-
148
- test("rejects duplicate function slugs", () => {
149
- const config = defineConfig(() => ({
150
- preview: {
151
- functions: [
152
- { name: "A", slug: "dup", source: "./a.ts" },
153
- { name: "B", slug: "dup", source: "./b.ts" },
154
- ],
155
- },
156
- }));
157
- expect(() =>
158
- resolveConfig(config, { name: "preview-1", exists: false }),
159
- ).toThrow(ConfigValidationError);
160
- });
161
- });
@@ -1,152 +0,0 @@
1
- import { parseDuration } from "./duration.js";
2
- import { ConfigValidationError } from "./errors.js";
3
- import { branchConfigSchema, formatZodIssues } from "./schema.js";
4
- import type {
5
- BranchTarget,
6
- Config,
7
- FunctionConfig,
8
- PreviewConfig,
9
- ResolvedBranchConfig,
10
- ResolvedPreviewConfig,
11
- } from "./types.js";
12
-
13
- /** Default deploy parameters applied to functions that omit them in `neon.ts`. */
14
- const DEFAULT_FUNCTION_RUNTIME = "nodejs24" as const;
15
- const DEFAULT_FUNCTION_MEMORY_MIB = 512 as const;
16
-
17
- const REGION_PREFIX = /^(aws|azure|gcp)-/;
18
-
19
- /**
20
- * Validate and freeze a Neon Platform branch policy.
21
- *
22
- * Used at the top of `neon.ts`:
23
- * ```ts
24
- * import { defineConfig } from "@neondatabase/config/v1";
25
- *
26
- * export default defineConfig((branch) => {
27
- * if (branch.name === "main") {
28
- * return { protected: true, auth: {} };
29
- * }
30
- * return { parent: "main", ttl: "7d" };
31
- * });
32
- * ```
33
- *
34
- * The `branch` parameter is a **read-only {@link BranchTarget} descriptor** of the branch
35
- * this policy invocation is deciding for — not a live branch handle. You don't mutate it
36
- * (`branch.protected = true` does nothing); you switch on its facts (`branch.name`,
37
- * `branch.isDefault`, `branch.exists`, …) and **return** the desired {@link BranchConfig}.
38
- * The same callback runs in two modes: against an existing branch (fields populated from
39
- * Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
40
- *
41
- * Pure function — no I/O, no side effects. The returned policy validates its output every
42
- * time it is evaluated so errors point at the concrete branch target that triggered them.
43
- */
44
- export function defineConfig<const C extends Config>(input: C): C {
45
- if (typeof input !== "function") {
46
- throw new ConfigValidationError([
47
- "defineConfig expects a function: `export default defineConfig((branch) => ({ ... }))`.",
48
- "Project-level config has moved to `neonctl link`; neon.ts now describes branch-level policy only.",
49
- ]);
50
- }
51
-
52
- return Object.freeze(input) as C;
53
- }
54
-
55
- /**
56
- * Evaluate a branch policy for a specific branch target and return a normalized config.
57
- */
58
- export function resolveConfig(
59
- config: Config,
60
- branch: BranchTarget,
61
- ): ResolvedBranchConfig {
62
- let raw: unknown;
63
- try {
64
- raw = config(Object.freeze({ ...branch }));
65
- } catch (cause) {
66
- throw new ConfigValidationError([
67
- `Config function threw while evaluating branch "${branch.name}".`,
68
- (cause as Error)?.message ?? String(cause),
69
- ]);
70
- }
71
-
72
- const parsed = branchConfigSchema.safeParse(raw);
73
- if (!parsed.success) {
74
- throw new ConfigValidationError(formatZodIssues(parsed.error));
75
- }
76
-
77
- const cfg = parsed.data;
78
- const issues: string[] = [];
79
- let ttlSeconds: number | undefined;
80
- if (cfg.ttl !== undefined) {
81
- const parsedTtl = parseDuration(cfg.ttl);
82
- if ("error" in parsedTtl) {
83
- issues.push(`ttl: ${parsedTtl.error}`);
84
- } else {
85
- ttlSeconds = parsedTtl.seconds;
86
- }
87
- }
88
- if (issues.length > 0) {
89
- throw new ConfigValidationError(issues);
90
- }
91
-
92
- const resolved: ResolvedBranchConfig = {
93
- authEnabled: isServiceEnabled(cfg.auth),
94
- dataApiEnabled: isServiceEnabled(cfg.dataApi),
95
- };
96
- if (cfg.parent !== undefined) resolved.parent = cfg.parent;
97
- if (ttlSeconds !== undefined) resolved.ttlSeconds = ttlSeconds;
98
- if (cfg.protected !== undefined) resolved.protected = cfg.protected;
99
- if (cfg.postgres) {
100
- resolved.postgres = {
101
- ...(cfg.postgres.computeSettings
102
- ? { computeSettings: { ...cfg.postgres.computeSettings } }
103
- : {}),
104
- };
105
- }
106
- if (cfg.preview) {
107
- resolved.preview = resolvePreviewConfig(cfg.preview);
108
- }
109
- return resolved;
110
- }
111
-
112
- function isServiceEnabled(service: { enabled?: boolean } | undefined): boolean {
113
- return service !== undefined && service.enabled !== false;
114
- }
115
-
116
- /**
117
- * Normalize a {@link PreviewConfig} into a {@link ResolvedPreviewConfig}: apply per-function
118
- * deploy defaults, default each bucket's access level to `private`, and collapse the
119
- * `aiGateway` toggle to a boolean using the same present-and-not-`false` rule as
120
- * `auth` / `dataApi`.
121
- */
122
- function resolvePreviewConfig(preview: PreviewConfig): ResolvedPreviewConfig {
123
- return {
124
- functions: (preview.functions ?? []).map(resolveFunctionConfig),
125
- buckets: (preview.buckets ?? []).map((bucket) => ({
126
- name: bucket.name,
127
- access: bucket.access ?? "private",
128
- })),
129
- aiGatewayEnabled: isServiceEnabled(preview.aiGateway),
130
- };
131
- }
132
-
133
- function resolveFunctionConfig(fn: FunctionConfig) {
134
- return {
135
- slug: fn.slug,
136
- name: fn.name,
137
- source: fn.source,
138
- env: { ...(fn.env ?? {}) },
139
- runtime: fn.runtime ?? DEFAULT_FUNCTION_RUNTIME,
140
- memoryMib: fn.memoryMib ?? DEFAULT_FUNCTION_MEMORY_MIB,
141
- };
142
- }
143
-
144
- /**
145
- * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes
146
- * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the
147
- * NeonApi adapter.
148
- */
149
- export function normalizeRegion(region: string): string {
150
- if (REGION_PREFIX.test(region)) return region;
151
- return `aws-${region}`;
152
- }