@neondatabase/config 0.4.2 → 0.7.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/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
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
- import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError } from "./lib/errors.js";
3
- import { CreateBranchInput, CreateBucketInput, CreateProjectInput, DeployFunctionInput, GetConnectionUriInput, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBucketSnapshot, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, UpdateBranchInput } from "./lib/neon-api.js";
1
+ import { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, ComputeUnit, Config, ConflictReport, CredentialPrincipalType, CredentialScope, DurationString, DurationUnit, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, PostgresConfig, PreviewInput, PreviewTuning, PushResult, ResolvedBranchConfig, ResolvedBucketConfig, ResolvedFunctionConfig, ResolvedPreviewConfig, ServiceToggle, ServiceToggleInput } from "./lib/types.js";
2
+ import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, isPlatformError } from "./lib/errors.js";
3
+ import { CreateBranchInput, CreateBucketInput, CreateCredentialInput, CreateProjectInput, DeployFunctionInput, GetConnectionUriInput, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBranchStorageSnapshot, NeonBucketSnapshot, NeonCredentialMeta, NeonCredentialSecret, NeonDataApiSnapshot, NeonDatabaseSnapshot, NeonEndpointSnapshot, NeonFunctionDeploymentSnapshot, NeonFunctionSnapshot, NeonProjectSnapshot, NeonRoleSnapshot, UpdateBranchInput } from "./lib/neon-api.js";
4
4
  import { createNeonApiFromOptions, resolveApiKey } from "./lib/auth.js";
5
+ import { CredentialFeatureFlags, credentialScopesSatisfied, deriveCredentialScopes } from "./lib/credentials.js";
5
6
  import { defineConfig, resolveConfig } from "./lib/define-config.js";
6
7
  import { DiffOptions, DiffResult, PlanStep, RemotePreviewState, RemoteServiceState, RemoteState, diffConfig } from "./lib/diff.js";
7
8
  import { LoadConfigOptions, loadConfigFromFile } from "./lib/loader.js";
8
9
  import { createRealNeonApi } from "./lib/neon-api-real.js";
9
10
  import { errors, schemas } from "./v1.js";
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 };
11
+ export { AppliedChange, BranchTarget, BranchTuning, BranchTuningFn, BucketAccessLevel, BucketDef, ComputeSettings, ComputeUnit, Config, ConfigLoadError, ConfigValidationError, ConflictReport, CreateBranchInput, CreateBucketInput, CreateCredentialInput, CreateProjectInput, CredentialFeatureFlags, CredentialPrincipalType, CredentialScope, DeployFunctionInput, DiffOptions, DiffResult, DurationString, DurationUnit, ErrorCode, FunctionDef, FunctionDevConfig, FunctionRuntime, FunctionTuning, GetConnectionUriInput, LoadConfigOptions, MissingContextError, NeonApi, NeonAuthSnapshot, NeonBranchSnapshot, NeonBranchStorageSnapshot, NeonBucketSnapshot, NeonCredentialMeta, NeonCredentialSecret, 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, credentialScopesSatisfied, defineConfig, deriveCredentialScopes, diffConfig, errors, isPlatformError, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError } from "./lib/errors.js";
1
+ import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, isPlatformError } from "./lib/errors.js";
2
2
  import { createRealNeonApi } from "./lib/neon-api-real.js";
3
3
  import { createNeonApiFromOptions, resolveApiKey } from "./lib/auth.js";
4
+ import { credentialScopesSatisfied, deriveCredentialScopes } from "./lib/credentials.js";
4
5
  import { defineConfig, resolveConfig } from "./lib/define-config.js";
5
6
  import { diffConfig } from "./lib/diff.js";
6
7
  import { loadConfigFromFile } from "./lib/loader.js";
7
8
  import { errors, schemas } from "./v1.js";
8
- export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, createNeonApiFromOptions, createRealNeonApi, defineConfig, diffConfig, errors, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
9
+ export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, createNeonApiFromOptions, createRealNeonApi, credentialScopesSatisfied, defineConfig, deriveCredentialScopes, diffConfig, errors, isPlatformError, loadConfigFromFile, resolveApiKey, resolveConfig, schemas };
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../../src/lib/auth.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { ErrorCode, PlatformError } from \"./errors.js\";\nimport type { NeonApi } from \"./neon-api.js\";\nimport { createRealNeonApi } from \"./neon-api-real.js\";\n\n/**\n * Minimal shape of `~/.config/neonctl/credentials.json` we read. `neonctl` writes more\n * fields (refresh_token, expires_at, …) but only `access_token` is what we need — it's a\n * Bearer token the Neon API accepts on the same endpoints `napi_*` API keys do.\n */\nexport interface NeonctlCredentials {\n\taccess_token: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Locate and read the OAuth credentials neonctl writes after `neon auth`.\n *\n * Resolution:\n * 1. `options.configDir` (explicit override — mirrors neonctl's `--config-dir` flag).\n * 2. `NEONCTL_CONFIG_DIR` environment variable.\n * 3. `<home>/.config/neonctl/credentials.json` (the neonctl default; `home` reads\n * `HOME`, falling back to `USERPROFILE` for Windows parity).\n *\n * Returns `null` (never throws) when the file is missing, unreadable, malformed, or has\n * no `access_token` — so callers can use this as a quiet fallback in a resolution chain\n * without try/catch noise.\n */\nexport function readNeonctlCredentials(\n\toptions: { configDir?: string } = {},\n): NeonctlCredentials | null {\n\tconst home = process.env.HOME ?? process.env.USERPROFILE;\n\tconst configDir =\n\t\toptions.configDir ??\n\t\tprocess.env.NEONCTL_CONFIG_DIR ??\n\t\t(home ? resolve(home, \".config\", \"neonctl\") : undefined);\n\tif (!configDir) return null;\n\n\tconst credentialsPath = resolve(configDir, \"credentials.json\");\n\tif (!existsSync(credentialsPath)) return null;\n\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(credentialsPath, \"utf-8\");\n\t} catch {\n\t\treturn null;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn null;\n\tconst obj = parsed as Record<string, unknown>;\n\tif (typeof obj.access_token !== \"string\" || obj.access_token === \"\")\n\t\treturn null;\n\treturn obj as NeonctlCredentials;\n}\n\n/**\n * Resolution chain for the Bearer token sent to the Neon API. Each entry wins over the\n * next:\n *\n * 1. `options.apiKey` (explicit).\n * 2. `NEON_API_KEY` environment variable.\n * 3. `access_token` from `~/.config/neonctl/credentials.json` (or `NEONCTL_CONFIG_DIR`).\n *\n * Returns `null` when no source provides one. Callers wrap the null case in a\n * `PLATFORM_MISSING_API_KEY` error with a message tailored to the operation.\n */\nexport function resolveApiKey(\n\toptions: { apiKey?: string; configDir?: string } = {},\n): { token: string; source: \"option\" | \"env\" | \"neonctl\" } | null {\n\tif (options.apiKey && options.apiKey.trim() !== \"\") {\n\t\treturn { token: options.apiKey.trim(), source: \"option\" };\n\t}\n\tconst envKey = process.env.NEON_API_KEY;\n\tif (typeof envKey === \"string\" && envKey.trim() !== \"\") {\n\t\treturn { token: envKey.trim(), source: \"env\" };\n\t}\n\tconst creds = readNeonctlCredentials(\n\t\toptions.configDir ? { configDir: options.configDir } : {},\n\t);\n\tif (creds) {\n\t\treturn { token: creds.access_token, source: \"neonctl\" };\n\t}\n\treturn null;\n}\n\n/** Trim trailing slashes and surrounding whitespace; treat empty as unset. */\nfunction normalizeApiHost(url: string | undefined): string | undefined {\n\tconst trimmed = url?.trim().replace(/\\/+$/, \"\");\n\treturn trimmed ? trimmed : undefined;\n}\n\n/**\n * Resolve the Neon API key via the standard chain (option → `NEON_API_KEY` env →\n * `~/.config/neonctl/credentials.json`) and construct a real {@link NeonApi} adapter from\n * it, or throw a uniform `PLATFORM_MISSING_API_KEY` error if no key can be found.\n *\n * The API host is resolved via: `options.apiHost` → `NEON_API_HOST` env → production\n * default (`https://console.neon.tech/api/v2`).\n *\n * Used by `pullConfig`, `pushConfig`, `fetchEnv`, and `branch` to build their default\n * `NeonApi` when the caller doesn't inject one. `operation` is the calling function's\n * name (e.g. `\"pushConfig\"`, `\"branch\"`) — it's prepended to the error message so users\n * can tell which call surfaced the missing key.\n */\nexport function createNeonApiFromOptions(\n\toperation: string,\n\toptions: {\n\t\tapiKey?: string;\n\t\tapiHost?: string;\n\t} = {},\n): NeonApi {\n\tconst resolved = resolveApiKey(\n\t\toptions.apiKey ? { apiKey: options.apiKey } : {},\n\t);\n\tif (resolved) {\n\t\tconst baseUrl =\n\t\t\tnormalizeApiHost(options.apiHost) ??\n\t\t\tnormalizeApiHost(process.env.NEON_API_HOST);\n\t\treturn createRealNeonApi({\n\t\t\tapiKey: resolved.token,\n\t\t\t...(baseUrl ? { baseUrl } : {}),\n\t\t});\n\t}\n\tthrow new PlatformError(\n\t\tErrorCode.MissingApiKey,\n\t\t[\n\t\t\t`${operation} has no Neon API key to work with.`,\n\t\t\t\"Tried (in order): `apiKey` option, NEON_API_KEY env, and `~/.config/neonctl/credentials.json`.\",\n\t\t\t\"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).\",\n\t\t\t\"Generate a key at https://console.neon.tech/app/settings/api-keys.\",\n\t\t].join(\" \"),\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAgB,uBACf,UAAkC,CAAC,GACP;CAC5B,MAAM,OAAO,QAAQ,IAAI,QAAQ,QAAQ,IAAI;CAC7C,MAAM,YACL,QAAQ,aACR,QAAQ,IAAI,uBACX,OAAO,QAAQ,MAAM,WAAW,SAAS,IAAI,KAAA;CAC/C,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,kBAAkB,QAAQ,WAAW,kBAAkB;CAC7D,IAAI,CAAC,WAAW,eAAe,GAAG,OAAO;CAEzC,IAAI;CACJ,IAAI;EACH,MAAM,aAAa,iBAAiB,OAAO;CAC5C,QAAQ;EACP,OAAO;CACR;CAEA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,GAAG;CACxB,QAAQ;EACP,OAAO;CACR;CAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO;CACR,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,iBAAiB,YAAY,IAAI,iBAAiB,IAChE,OAAO;CACR,OAAO;AACR;;;;;;;;;;;;AAaA,SAAgB,cACf,UAAmD,CAAC,GACa;CACjE,IAAI,QAAQ,UAAU,QAAQ,OAAO,KAAK,MAAM,IAC/C,OAAO;EAAE,OAAO,QAAQ,OAAO,KAAK;EAAG,QAAQ;CAAS;CAEzD,MAAM,SAAS,QAAQ,IAAI;CAC3B,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM,IACnD,OAAO;EAAE,OAAO,OAAO,KAAK;EAAG,QAAQ;CAAM;CAE9C,MAAM,QAAQ,uBACb,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC,CACzD;CACA,IAAI,OACH,OAAO;EAAE,OAAO,MAAM;EAAc,QAAQ;CAAU;CAEvD,OAAO;AACR;;AAGA,SAAS,iBAAiB,KAA6C;CACtE,MAAM,UAAU,KAAK,KAAK,EAAE,QAAQ,QAAQ,EAAE;CAC9C,OAAO,UAAU,UAAU,KAAA;AAC5B;;;;;;;;;;;;;;AAeA,SAAgB,yBACf,WACA,UAGI,CAAC,GACK;CACV,MAAM,WAAW,cAChB,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAChD;CACA,IAAI,UAAU;EACb,MAAM,UACL,iBAAiB,QAAQ,OAAO,KAChC,iBAAiB,QAAQ,IAAI,aAAa;EAC3C,OAAO,kBAAkB;GACxB,QAAQ,SAAS;GACjB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC9B,CAAC;CACF;CACA,MAAM,IAAI,cACT,UAAU,eACV;EACC,GAAG,UAAU;EACb;EACA;EACA;CACD,EAAE,KAAK,GAAG,CACX;AACD"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../../src/lib/auth.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { ErrorCode, PlatformError } from \"./errors.js\";\nimport type { NeonApi } from \"./neon-api.js\";\nimport { createRealNeonApi } from \"./neon-api-real.js\";\n\n/**\n * Minimal shape of `~/.config/neonctl/credentials.json` we read. `neonctl` writes more\n * fields (refresh_token, expires_at, …) but only `access_token` is what we need — it's a\n * Bearer token the Neon API accepts on the same endpoints `napi_*` API keys do.\n */\nexport interface NeonctlCredentials {\n\taccess_token: string;\n\t[key: string]: unknown;\n}\n\n/**\n * Locate and read the OAuth credentials neonctl writes after `neon auth`.\n *\n * Resolution:\n * 1. `options.configDir` (explicit override — mirrors neonctl's `--config-dir` flag).\n * 2. `NEONCTL_CONFIG_DIR` environment variable.\n * 3. `<home>/.config/neonctl/credentials.json` (the neonctl default; `home` reads\n * `HOME`, falling back to `USERPROFILE` for Windows parity).\n *\n * Returns `null` (never throws) when the file is missing, unreadable, malformed, or has\n * no `access_token` — so callers can use this as a quiet fallback in a resolution chain\n * without try/catch noise.\n */\nexport function readNeonctlCredentials(\n\toptions: { configDir?: string } = {},\n): NeonctlCredentials | null {\n\tconst home = process.env.HOME ?? process.env.USERPROFILE;\n\tconst configDir =\n\t\toptions.configDir ??\n\t\tprocess.env.NEONCTL_CONFIG_DIR ??\n\t\t(home ? resolve(home, \".config\", \"neonctl\") : undefined);\n\tif (!configDir) return null;\n\n\tconst credentialsPath = resolve(configDir, \"credentials.json\");\n\tif (!existsSync(credentialsPath)) return null;\n\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(credentialsPath, \"utf-8\");\n\t} catch {\n\t\treturn null;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed))\n\t\treturn null;\n\tconst obj = parsed as Record<string, unknown>;\n\tif (typeof obj.access_token !== \"string\" || obj.access_token === \"\")\n\t\treturn null;\n\treturn obj as NeonctlCredentials;\n}\n\n/**\n * Resolution chain for the Bearer token sent to the Neon API. Each entry wins over the\n * next:\n *\n * 1. `options.apiKey` (explicit).\n * 2. `NEON_API_KEY` environment variable.\n * 3. `access_token` from `~/.config/neonctl/credentials.json` (or `NEONCTL_CONFIG_DIR`).\n *\n * Returns `null` when no source provides one. Callers wrap the null case in a\n * `PLATFORM_MISSING_API_KEY` error with a message tailored to the operation.\n */\nexport function resolveApiKey(\n\toptions: { apiKey?: string; configDir?: string } = {},\n): { token: string; source: \"option\" | \"env\" | \"neonctl\" } | null {\n\tif (options.apiKey && options.apiKey.trim() !== \"\") {\n\t\treturn { token: options.apiKey.trim(), source: \"option\" };\n\t}\n\tconst envKey = process.env.NEON_API_KEY;\n\tif (typeof envKey === \"string\" && envKey.trim() !== \"\") {\n\t\treturn { token: envKey.trim(), source: \"env\" };\n\t}\n\tconst creds = readNeonctlCredentials(\n\t\toptions.configDir ? { configDir: options.configDir } : {},\n\t);\n\tif (creds) {\n\t\treturn { token: creds.access_token, source: \"neonctl\" };\n\t}\n\treturn null;\n}\n\n/** Trim trailing slashes and surrounding whitespace; treat empty as unset. */\nfunction normalizeApiHost(url: string | undefined): string | undefined {\n\tconst trimmed = url?.trim().replace(/\\/+$/, \"\");\n\treturn trimmed ? trimmed : undefined;\n}\n\n/**\n * Resolve the Neon API key via the standard chain (option → `NEON_API_KEY` env →\n * `~/.config/neonctl/credentials.json`) and construct a real {@link NeonApi} adapter from\n * it, or throw a uniform `PLATFORM_MISSING_API_KEY` error if no key can be found.\n *\n * The API host is resolved via: `options.apiHost` → `NEON_API_HOST` env → production\n * default (`https://console.neon.tech/api/v2`).\n *\n * Used by `pullConfig`, `pushConfig`, `fetchEnv`, and `branch` to build their default\n * `NeonApi` when the caller doesn't inject one. `operation` is the calling function's\n * name (e.g. `\"pushConfig\"`, `\"branch\"`) — it's prepended to the error message so users\n * can tell which call surfaced the missing key.\n */\nexport function createNeonApiFromOptions(\n\toperation: string,\n\toptions: {\n\t\tapiKey?: string;\n\t\tapiHost?: string;\n\t} = {},\n): NeonApi {\n\tconst resolved = resolveApiKey(\n\t\toptions.apiKey ? { apiKey: options.apiKey } : {},\n\t);\n\tif (resolved) {\n\t\tconst baseUrl =\n\t\t\tnormalizeApiHost(options.apiHost) ??\n\t\t\tnormalizeApiHost(process.env.NEON_API_HOST);\n\t\treturn createRealNeonApi({\n\t\t\tapiKey: resolved.token,\n\t\t\t...(baseUrl ? { baseUrl } : {}),\n\t\t});\n\t}\n\tthrow new PlatformError(\n\t\tErrorCode.MissingApiKey,\n\t\t[\n\t\t\t`${operation} has no Neon API key to work with.`,\n\t\t\t\"Tried (in order): `apiKey` option, NEON_API_KEY env, and `~/.config/neonctl/credentials.json`.\",\n\t\t\t\"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).\",\n\t\t\t\"Generate a key at https://console.neon.tech/app/settings/api-keys.\",\n\t\t].join(\" \"),\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAgB,uBACf,UAAkC,CAAC,GACP;CAC5B,MAAM,OAAO,QAAQ,IAAI,QAAQ,QAAQ,IAAI;CAC7C,MAAM,YACL,QAAQ,aACR,QAAQ,IAAI,uBACX,OAAO,QAAQ,MAAM,WAAW,SAAS,IAAI,KAAA;CAC/C,IAAI,CAAC,WAAW,OAAO;CAEvB,MAAM,kBAAkB,QAAQ,WAAW,kBAAkB;CAC7D,IAAI,CAAC,WAAW,eAAe,GAAG,OAAO;CAEzC,IAAI;CACJ,IAAI;EACH,MAAM,aAAa,iBAAiB,OAAO;CAC5C,QAAQ;EACP,OAAO;CACR;CAEA,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,GAAG;CACxB,QAAQ;EACP,OAAO;CACR;CAEA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACxE,OAAO;CACR,MAAM,MAAM;CACZ,IAAI,OAAO,IAAI,iBAAiB,YAAY,IAAI,iBAAiB,IAChE,OAAO;CACR,OAAO;AACR;;;;;;;;;;;;AAaA,SAAgB,cACf,UAAmD,CAAC,GACa;CACjE,IAAI,QAAQ,UAAU,QAAQ,OAAO,KAAK,MAAM,IAC/C,OAAO;EAAE,OAAO,QAAQ,OAAO,KAAK;EAAG,QAAQ;CAAS;CAEzD,MAAM,SAAS,QAAQ,IAAI;CAC3B,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM,IACnD,OAAO;EAAE,OAAO,OAAO,KAAK;EAAG,QAAQ;CAAM;CAE9C,MAAM,QAAQ,uBACb,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC,CACzD;CACA,IAAI,OACH,OAAO;EAAE,OAAO,MAAM;EAAc,QAAQ;CAAU;CAEvD,OAAO;AACR;;AAGA,SAAS,iBAAiB,KAA6C;CACtE,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE;CAC9C,OAAO,UAAU,UAAU,KAAA;AAC5B;;;;;;;;;;;;;;AAeA,SAAgB,yBACf,WACA,UAGI,CAAC,GACK;CACV,MAAM,WAAW,cAChB,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAChD;CACA,IAAI,UAAU;EACb,MAAM,UACL,iBAAiB,QAAQ,OAAO,KAChC,iBAAiB,QAAQ,IAAI,aAAa;EAC3C,OAAO,kBAAkB;GACxB,QAAQ,SAAS;GACjB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC9B,CAAC;CACF;CACA,MAAM,IAAI,cACT,UAAU,eACV;EACC,GAAG,UAAU;EACb;EACA;EACA;CACD,CAAC,CAAC,KAAK,GAAG,CACX;AACD"}
@@ -0,0 +1,37 @@
1
+ import { CredentialScope } from "./types.js";
2
+
3
+ //#region src/lib/credentials.d.ts
4
+
5
+ /**
6
+ * Which branch-scoped Preview features a policy enables, reduced to the booleans that drive
7
+ * credential scopes. Decoupled from the full `Config` / `ResolvedPreviewConfig` shapes so it
8
+ * can be computed from either the static policy (`parseEnv`) or the resolved branch config
9
+ * (`fetchEnv`).
10
+ */
11
+ interface CredentialFeatureFlags {
12
+ /** At least one object-storage bucket is declared (grants `storage:read` + `storage:write`). */
13
+ storage: boolean;
14
+ /** The AI Gateway is enabled (grants `ai_gateway:invoke`). */
15
+ aiGateway: boolean;
16
+ /** At least one Neon Function is declared (grants `functions:invoke`). */
17
+ functions: boolean;
18
+ }
19
+ /**
20
+ * Derive the set of {@link CredentialScope}s a branch credential needs from the policy's
21
+ * enabled Preview features. Deterministic and order-stable (handy for diffing / round-trip
22
+ * comparisons). Returns an empty array when no credential-bearing feature is enabled — the
23
+ * signal that **no credential should be minted** (and the credentials endpoint never
24
+ * touched), which is what keeps the non-Preview path byte-for-byte unchanged.
25
+ */
26
+ declare function deriveCredentialScopes(flags: CredentialFeatureFlags): CredentialScope[];
27
+ /**
28
+ * Whether a credential granted `granted` scopes can satisfy a policy that needs `desired`
29
+ * scopes — i.e. `desired ⊆ granted`. Used to decide whether an already-persisted credential
30
+ * can be **reused** as-is, or must be re-minted because the policy now needs a scope the old
31
+ * credential lacks (e.g. the user added the AI Gateway after first pulling a storage-only
32
+ * credential).
33
+ */
34
+ declare function credentialScopesSatisfied(granted: readonly CredentialScope[], desired: readonly CredentialScope[]): boolean;
35
+ //#endregion
36
+ export { CredentialFeatureFlags, credentialScopesSatisfied, deriveCredentialScopes };
37
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","names":[],"sources":["../../src/lib/credentials.ts"],"mappings":";;;;;;AAQA;AAgBA;;;AAEG,UAlBc,sBAAA,CAkBd;EAAe;EAqBF,OAAA,EAAA,OAAA;EAAyB;WACtB,EAAA,OAAA;;EACe,SAAA,EAAA,OAAA;;;;;;;;;iBAzBlB,sBAAA,QACR,yBACL;;;;;;;;iBAqBa,yBAAA,mBACG,qCACA"}
@@ -0,0 +1,30 @@
1
+ //#region src/lib/credentials.ts
2
+ /**
3
+ * Derive the set of {@link CredentialScope}s a branch credential needs from the policy's
4
+ * enabled Preview features. Deterministic and order-stable (handy for diffing / round-trip
5
+ * comparisons). Returns an empty array when no credential-bearing feature is enabled — the
6
+ * signal that **no credential should be minted** (and the credentials endpoint never
7
+ * touched), which is what keeps the non-Preview path byte-for-byte unchanged.
8
+ */
9
+ function deriveCredentialScopes(flags) {
10
+ const scopes = [];
11
+ if (flags.storage) scopes.push("storage:read", "storage:write");
12
+ if (flags.aiGateway) scopes.push("ai_gateway:invoke");
13
+ if (flags.functions) scopes.push("functions:invoke");
14
+ return scopes;
15
+ }
16
+ /**
17
+ * Whether a credential granted `granted` scopes can satisfy a policy that needs `desired`
18
+ * scopes — i.e. `desired ⊆ granted`. Used to decide whether an already-persisted credential
19
+ * can be **reused** as-is, or must be re-minted because the policy now needs a scope the old
20
+ * credential lacks (e.g. the user added the AI Gateway after first pulling a storage-only
21
+ * credential).
22
+ */
23
+ function credentialScopesSatisfied(granted, desired) {
24
+ const grantedSet = new Set(granted);
25
+ return desired.every((scope) => grantedSet.has(scope));
26
+ }
27
+ //#endregion
28
+ export { credentialScopesSatisfied, deriveCredentialScopes };
29
+
30
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","names":[],"sources":["../../src/lib/credentials.ts"],"sourcesContent":["import type { CredentialScope } from \"./types.js\";\n\n/**\n * Which branch-scoped Preview features a policy enables, reduced to the booleans that drive\n * credential scopes. Decoupled from the full `Config` / `ResolvedPreviewConfig` shapes so it\n * can be computed from either the static policy (`parseEnv`) or the resolved branch config\n * (`fetchEnv`).\n */\nexport interface CredentialFeatureFlags {\n\t/** At least one object-storage bucket is declared (grants `storage:read` + `storage:write`). */\n\tstorage: boolean;\n\t/** The AI Gateway is enabled (grants `ai_gateway:invoke`). */\n\taiGateway: boolean;\n\t/** At least one Neon Function is declared (grants `functions:invoke`). */\n\tfunctions: boolean;\n}\n\n/**\n * Derive the set of {@link CredentialScope}s a branch credential needs from the policy's\n * enabled Preview features. Deterministic and order-stable (handy for diffing / round-trip\n * comparisons). Returns an empty array when no credential-bearing feature is enabled — the\n * signal that **no credential should be minted** (and the credentials endpoint never\n * touched), which is what keeps the non-Preview path byte-for-byte unchanged.\n */\nexport function deriveCredentialScopes(\n\tflags: CredentialFeatureFlags,\n): CredentialScope[] {\n\tconst scopes: CredentialScope[] = [];\n\tif (flags.storage) {\n\t\tscopes.push(\"storage:read\", \"storage:write\");\n\t}\n\tif (flags.aiGateway) {\n\t\tscopes.push(\"ai_gateway:invoke\");\n\t}\n\tif (flags.functions) {\n\t\tscopes.push(\"functions:invoke\");\n\t}\n\treturn scopes;\n}\n\n/**\n * Whether a credential granted `granted` scopes can satisfy a policy that needs `desired`\n * scopes — i.e. `desired ⊆ granted`. Used to decide whether an already-persisted credential\n * can be **reused** as-is, or must be re-minted because the policy now needs a scope the old\n * credential lacks (e.g. the user added the AI Gateway after first pulling a storage-only\n * credential).\n */\nexport function credentialScopesSatisfied(\n\tgranted: readonly CredentialScope[],\n\tdesired: readonly CredentialScope[],\n): boolean {\n\tconst grantedSet = new Set(granted);\n\treturn desired.every((scope) => grantedSet.has(scope));\n}\n"],"mappings":";;;;;;;;AAwBA,SAAgB,uBACf,OACoB;CACpB,MAAM,SAA4B,CAAC;CACnC,IAAI,MAAM,SACT,OAAO,KAAK,gBAAgB,eAAe;CAE5C,IAAI,MAAM,WACT,OAAO,KAAK,mBAAmB;CAEhC,IAAI,MAAM,WACT,OAAO,KAAK,kBAAkB;CAE/B,OAAO;AACR;;;;;;;;AASA,SAAgB,0BACf,SACA,SACU;CACV,MAAM,aAAa,IAAI,IAAI,OAAO;CAClC,OAAO,QAAQ,OAAO,UAAU,WAAW,IAAI,KAAK,CAAC;AACtD"}
@@ -1 +1 @@
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":"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"}
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,CAAC,CAAC,KAAK,CAAC,MAAM,SACb,sBAAsB,MAAM,KAAK,SAAS,SAAS,CAAC,CAAC,CAS7C;EACR,SARe,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,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"}
@@ -44,29 +44,21 @@ type PlanStep = {
44
44
  branchName: string;
45
45
  bucketName: string;
46
46
  accessLevel: BucketAccessLevel;
47
- } | {
48
- kind: "create-function";
49
- projectId: string;
50
- branchId: string;
51
- branchName: string;
52
- fn: ResolvedFunctionConfig;
53
47
  } | {
54
48
  /**
55
- * Deploy (or re-deploy) code to a function. Always planned for every desired
56
- * function — deployments are versioned and the newest becomes active, so a push
57
- * ships the current source each time. `functionExists` tells `pushConfig` whether
58
- * it must create the function first (covered by a preceding `create-function` step).
49
+ * Deploy code to a function. Planned for every desired function — deployments are
50
+ * versioned and the newest becomes active, so a push ships the current source each
51
+ * time. Neon has no separate "create function" endpoint: the first deployment to a
52
+ * slug creates the function. `functionExists` therefore only drives whether this
53
+ * surfaces as a `create` (first deploy) or an `update` (re-deploy).
59
54
  */
60
55
  kind: "deploy-function";
61
56
  projectId: string;
62
57
  branchId: string;
63
58
  branchName: string;
64
59
  fn: ResolvedFunctionConfig;
65
- } | {
66
- kind: "enable-ai-gateway";
67
- projectId: string;
68
- branchId: string;
69
- branchName: string;
60
+ /** Whether the function already existed remotely when the plan was computed. */
61
+ functionExists: boolean;
70
62
  };
71
63
  interface RemoteServiceState {
72
64
  databaseName: string;
@@ -80,7 +72,6 @@ interface RemoteServiceState {
80
72
  interface RemotePreviewState {
81
73
  buckets: NeonBucketSnapshot[];
82
74
  functions: NeonFunctionSnapshot[];
83
- aiGatewayEnabled: boolean;
84
75
  }
85
76
  interface RemoteState {
86
77
  projectId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","names":[],"sources":["../../src/lib/diff.ts"],"mappings":";;;;;;;AAkBA;;AAoBa,KApBD,QAAA,GAoBC;MAsBG,EAAA,mBAAA;WAOT,EAAA,MAAA;UAaA,EAAA,MAAA;EAAsB,UAAA,EAAA,MAAA;EASZ,SAAA,EAAA,MAAA,GAAA,IAAkB;AAUnC,CAAA,GAAiB;EAAkB,IAAA,EAAA,yBAAA;WACzB,EAAA,MAAA;UACE,EAAA,MAAA;EAAoB,UAAA,EAAA,MAAA;EAIf,SAAA,EAAA,OAAW;CAAA,GAAA;MAEnB,EAAA,iBAAA;WACG,EAAA,MAAA;YACD,EAAA,MAAA;YACA,EAAA,MAAA;EAAkB,QAAA,EAxEhB,eAwEgB;AAG7B,CAAA,GAAiB;EAQA,IAAA,EAAA,aAAU;EAAA,SAAA,EAAA,MAAA;UACpB,EAAA,MAAA;YACK,EAAA,MAAA;EAAc,YAAA,CAAA,EAAA,MAAA;AAM1B,CAAA,GAAgB;EAAU,IAAA,EAAA,iBAAA;WACjB,EAAA,MAAA;UACA,EAAA,MAAA;YACC,EAAA,MAAA;cACP,EAAA,MAAA;AAAU,CAAA,GAAA;;;;;;eAzEG;;;;;;MAOT;;;;;;;;;;;;MAaA;;;;;;;UASU,kBAAA;;;;;;;;;UAUA,kBAAA;WACP;aACE;;;UAIK,WAAA;;UAER;aACG;YACD;YACA;;UAGM,WAAA;;;;;;;UAQA,UAAA;QACV;aACK;;;;;iBAMI,UAAA,SACP,8BACA,sBACC,cACP"}
1
+ {"version":3,"file":"diff.d.ts","names":[],"sources":["../../src/lib/diff.ts"],"mappings":";;;;;;;AAkBA;;AAoBa,KApBD,QAAA,GAoBC;MAsBG,EAAA,mBAAA;WAcT,EAAA,MAAA;EAAsB,QAAA,EAAA,MAAA;EAKZ,UAAA,EAAA,MAAA;EAUA,SAAA,EAAA,MAAA,GAAA,IAAkB;CAAA,GAAA;MACzB,EAAA,yBAAA;WACE,EAAA,MAAA;EAAoB,QAAA,EAAA,MAAA;EAGf,UAAA,EAAA,MAAW;EAAA,SAAA,EAAA,OAAA;;MAGhB,EAAA,iBAAA;WACD,EAAA,MAAA;YACA,EAAA,MAAA;EAAkB,UAAA,EAAA,MAAA;EAGZ,QAAA,EAhEJ,eAgEe;AAQ5B,CAAA,GAAiB;EAAU,IAAA,EAAA,aAAA;WACpB,EAAA,MAAA;UACK,EAAA,MAAA;EAAc,UAAA,EAAA,MAAA;EAMV,YAAA,CAAU,EAAA,MAAA;CAAA,GAAA;MACjB,EAAA,iBAAA;WACA,EAAA,MAAA;UACC,EAAA,MAAA;YACP,EAAA,MAAA;EAAU,YAAA,EAAA,MAAA;;;;;;;eA9DG;;;;;;;;;;;;;MAcT;;;;UAKU,kBAAA;;;;;;;;;UAUA,kBAAA;WACP;aACE;;UAGK,WAAA;;UAER;aACG;YACD;YACA;;UAGM,WAAA;;;;;;;UAQA,UAAA;QACV;aACK;;;;;iBAMI,UAAA,SACP,8BACA,sBACC,cACP"}
package/dist/lib/diff.js CHANGED
@@ -28,14 +28,21 @@ function diffConfig(config, remote, options) {
28
28
  };
29
29
  }
30
30
  /**
31
- * Plan Preview features (functions, buckets, AI Gateway). Like {@link diffServices}, this
32
- * is **additive**: it creates desired buckets/functions and enables the AI Gateway, but
33
- * never deletes buckets/functions or disables the gateway. Teardown is destructive, so it
34
- * stays explicit/manual — matching the existing auth / dataApi behaviour.
31
+ * Plan Preview features (functions, buckets). Like {@link diffServices}, this is
32
+ * **additive**: it creates desired buckets and (re-)deploys functions, but never deletes
33
+ * them. Teardown is destructive, so it stays explicit/manual — matching the existing
34
+ * auth / dataApi behaviour.
35
+ *
36
+ * The AI Gateway is intentionally NOT planned here: it is always available on a branch
37
+ * (credential-gated, not per-branch provisioned), so `preview.aiGateway` produces no plan
38
+ * step — it only drives the branch credential's `ai_gateway:invoke` scope and the gateway
39
+ * env vars (`@neondatabase/env`). There is nothing to create, and nothing to probe.
35
40
  *
36
41
  * Functions are always (re-)deployed: deployments are versioned and the newest becomes
37
- * active, so each push ships the current source. A `create-function` step precedes the
38
- * `deploy-function` step when the function does not yet exist remotely.
42
+ * active, so each push ships the current source. There is no separate create step Neon
43
+ * creates the function on its first deployment so a single `deploy-function` step is
44
+ * emitted per desired function, carrying `functionExists` so the apply can report it as a
45
+ * create (first deploy) or an update (re-deploy).
39
46
  */
40
47
  function diffPreview(args) {
41
48
  const { config, remote, plan } = args;
@@ -43,8 +50,7 @@ function diffPreview(args) {
43
50
  if (!preview) return;
44
51
  const state = remote.preview ?? {
45
52
  buckets: [],
46
- functions: [],
47
- aiGatewayEnabled: false
53
+ functions: []
48
54
  };
49
55
  for (const bucket of preview.buckets) {
50
56
  if (state.buckets.some((b) => b.name === bucket.name)) continue;
@@ -58,27 +64,16 @@ function diffPreview(args) {
58
64
  });
59
65
  }
60
66
  for (const fn of preview.functions) {
61
- if (!state.functions.some((f) => f.slug === fn.slug)) plan.push({
62
- kind: "create-function",
63
- projectId: remote.projectId,
64
- branchId: remote.branch.id,
65
- branchName: remote.branch.name,
66
- fn
67
- });
67
+ const exists = state.functions.some((f) => f.slug === fn.slug);
68
68
  plan.push({
69
69
  kind: "deploy-function",
70
70
  projectId: remote.projectId,
71
71
  branchId: remote.branch.id,
72
72
  branchName: remote.branch.name,
73
- fn
73
+ fn,
74
+ functionExists: exists
74
75
  });
75
76
  }
76
- if (preview.aiGatewayEnabled && !state.aiGatewayEnabled) plan.push({
77
- kind: "enable-ai-gateway",
78
- projectId: remote.projectId,
79
- branchId: remote.branch.id,
80
- branchName: remote.branch.name
81
- });
82
77
  }
83
78
  /**
84
79
  * Plan additive branch-scoped integrations. Disabling remains explicit/manual because
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","names":[],"sources":["../../src/lib/diff.ts"],"sourcesContent":["import type {\n\tNeonBranchSnapshot,\n\tNeonBucketSnapshot,\n\tNeonEndpointSnapshot,\n\tNeonFunctionSnapshot,\n} from \"./neon-api.js\";\nimport type {\n\tBucketAccessLevel,\n\tComputeSettings,\n\tConflictReport,\n\tResolvedBranchConfig,\n\tResolvedFunctionConfig,\n} from \"./types.js\";\n\n/**\n * A planned action to perform a single mutation against the Neon API. The diff engine\n * produces a list of these for `pushConfig` to execute (or report).\n */\nexport type PlanStep =\n\t| {\n\t\t\tkind: \"update-branch-ttl\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\texpiresAt: string | null;\n\t }\n\t| {\n\t\t\tkind: \"update-branch-protected\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tprotected: boolean;\n\t }\n\t| {\n\t\t\tkind: \"update-endpoint\";\n\t\t\tprojectId: string;\n\t\t\tbranchName: string;\n\t\t\tendpointId: string;\n\t\t\tsettings: ComputeSettings;\n\t }\n\t| {\n\t\t\tkind: \"enable-auth\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName?: string;\n\t }\n\t| {\n\t\t\tkind: \"enable-data-api\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName: string;\n\t }\n\t| {\n\t\t\tkind: \"create-bucket\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tbucketName: string;\n\t\t\taccessLevel: BucketAccessLevel;\n\t }\n\t| {\n\t\t\tkind: \"create-function\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tfn: ResolvedFunctionConfig;\n\t }\n\t| {\n\t\t\t/**\n\t\t\t * Deploy (or re-deploy) code to a function. Always planned for every desired\n\t\t\t * function — deployments are versioned and the newest becomes active, so a push\n\t\t\t * ships the current source each time. `functionExists` tells `pushConfig` whether\n\t\t\t * it must create the function first (covered by a preceding `create-function` step).\n\t\t\t */\n\t\t\tkind: \"deploy-function\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tfn: ResolvedFunctionConfig;\n\t }\n\t| {\n\t\t\tkind: \"enable-ai-gateway\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t };\n\nexport interface RemoteServiceState {\n\tdatabaseName: string;\n\tauthEnabled: boolean;\n\tdataApiEnabled: boolean;\n}\n\n/**\n * Snapshot of the branch's current Preview-feature state. Absent (`undefined`) when the\n * policy has no `preview` block — `pushConfig` only fetches this when needed.\n */\nexport interface RemotePreviewState {\n\tbuckets: NeonBucketSnapshot[];\n\tfunctions: NeonFunctionSnapshot[];\n\taiGatewayEnabled: boolean;\n}\n\nexport interface RemoteState {\n\tprojectId: string;\n\tbranch: NeonBranchSnapshot;\n\tendpoint?: NeonEndpointSnapshot;\n\tservices: RemoteServiceState;\n\tpreview?: RemotePreviewState;\n}\n\nexport interface DiffOptions {\n\t/**\n\t * Apply mutable drift on the selected branch as plan steps instead of conflicts.\n\t * Default: `false`.\n\t */\n\tupdateExisting: boolean;\n}\n\nexport interface DiffResult {\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\n/**\n * Diff desired branch policy against the selected remote branch. Pure function.\n */\nexport function diffConfig(\n\tconfig: ResolvedBranchConfig,\n\tremote: RemoteState,\n\toptions: DiffOptions,\n): DiffResult {\n\tconst conflicts: ConflictReport[] = [];\n\tconst plan: PlanStep[] = [];\n\tdiffBranchConfig({ config, remote, options, plan, conflicts });\n\tdiffServices({ config, remote, plan });\n\tdiffPreview({ config, remote, plan });\n\treturn { plan, conflicts };\n}\n\n/**\n * Plan Preview features (functions, buckets, AI Gateway). Like {@link diffServices}, this\n * is **additive**: it creates desired buckets/functions and enables the AI Gateway, but\n * never deletes buckets/functions or disables the gateway. Teardown is destructive, so it\n * stays explicit/manual — matching the existing auth / dataApi behaviour.\n *\n * Functions are always (re-)deployed: deployments are versioned and the newest becomes\n * active, so each push ships the current source. A `create-function` step precedes the\n * `deploy-function` step when the function does not yet exist remotely.\n */\nfunction diffPreview(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\tplan: PlanStep[];\n}): void {\n\tconst { config, remote, plan } = args;\n\tconst preview = config.preview;\n\tif (!preview) return;\n\t// `remote.preview` is only fetched when the policy has a preview block; treat a missing\n\t// snapshot as \"nothing exists yet\" so the diff is still well-defined.\n\tconst state: RemotePreviewState = remote.preview ?? {\n\t\tbuckets: [],\n\t\tfunctions: [],\n\t\taiGatewayEnabled: false,\n\t};\n\n\tfor (const bucket of preview.buckets) {\n\t\tif (state.buckets.some((b) => b.name === bucket.name)) continue;\n\t\tplan.push({\n\t\t\tkind: \"create-bucket\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tbucketName: bucket.name,\n\t\t\taccessLevel: bucket.access,\n\t\t});\n\t}\n\n\tfor (const fn of preview.functions) {\n\t\tconst exists = state.functions.some((f) => f.slug === fn.slug);\n\t\tif (!exists) {\n\t\t\tplan.push({\n\t\t\t\tkind: \"create-function\",\n\t\t\t\tprojectId: remote.projectId,\n\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\tbranchName: remote.branch.name,\n\t\t\t\tfn,\n\t\t\t});\n\t\t}\n\t\tplan.push({\n\t\t\tkind: \"deploy-function\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tfn,\n\t\t});\n\t}\n\n\tif (preview.aiGatewayEnabled && !state.aiGatewayEnabled) {\n\t\tplan.push({\n\t\t\tkind: \"enable-ai-gateway\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t});\n\t}\n}\n\n/**\n * Plan additive branch-scoped integrations. Disabling remains explicit/manual because\n * teardown is destructive.\n */\nfunction diffServices(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\tplan: PlanStep[];\n}): void {\n\tconst { config, remote, plan } = args;\n\tconst state = remote.services;\n\tif (config.authEnabled && !state.authEnabled) {\n\t\tconst step: PlanStep = {\n\t\t\tkind: \"enable-auth\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t};\n\t\tif (state.databaseName) step.databaseName = state.databaseName;\n\t\tplan.push(step);\n\t}\n\tif (config.dataApiEnabled && !state.dataApiEnabled) {\n\t\tplan.push({\n\t\t\tkind: \"enable-data-api\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tdatabaseName: state.databaseName,\n\t\t});\n\t}\n}\n\ninterface BranchConfigArgs {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\toptions: DiffOptions;\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\nfunction diffBranchConfig(args: BranchConfigArgs): void {\n\tconst { config, remote, options, plan, conflicts } = args;\n\tconst branchName = remote.branch.name;\n\tconst computeSettings = config.postgres?.computeSettings;\n\n\tif (computeSettings) {\n\t\tconst endpoint = remote.endpoint;\n\t\tif (!endpoint) {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"endpoint\",\n\t\t\t\tcurrent: undefined,\n\t\t\t\tdesired: computeSettings,\n\t\t\t\treason: \"Branch has no read-write endpoint; cannot apply compute settings.\",\n\t\t\t});\n\t\t} else {\n\t\t\tconst drift = computeDriftBetween(computeSettings, endpoint);\n\t\t\tif (drift) {\n\t\t\t\tif (options.updateExisting) {\n\t\t\t\t\tplan.push({\n\t\t\t\t\t\tkind: \"update-endpoint\",\n\t\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\t\tbranchName,\n\t\t\t\t\t\tendpointId: endpoint.id,\n\t\t\t\t\t\tsettings: computeSettings,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconflicts.push({\n\t\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\t\tfield: \"computeSettings\",\n\t\t\t\t\t\tcurrent: drift.current,\n\t\t\t\t\t\tdesired: drift.desired,\n\t\t\t\t\t\treason: \"Existing branch has different compute settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (\n\t\tconfig.protected !== undefined &&\n\t\tconfig.protected !== remote.branch.protected\n\t) {\n\t\tif (options.updateExisting) {\n\t\t\tplan.push({\n\t\t\t\tkind: \"update-branch-protected\",\n\t\t\t\tprojectId: remote.projectId,\n\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\tbranchName,\n\t\t\t\tprotected: config.protected,\n\t\t\t});\n\t\t} else {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"protected\",\n\t\t\t\tcurrent: remote.branch.protected,\n\t\t\t\tdesired: config.protected,\n\t\t\t\treason: \"Existing branch has a different `protected` flag. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t});\n\t\t}\n\t}\n\n\tif (config.ttlSeconds !== undefined) {\n\t\tconst current = remote.branch.expiresAt\n\t\t\t? Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.round(\n\t\t\t\t\t\t(Date.parse(remote.branch.expiresAt) - Date.now()) /\n\t\t\t\t\t\t\t1000,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t: undefined;\n\t\tif (\n\t\t\tcurrent === undefined ||\n\t\t\tMath.abs(current - config.ttlSeconds) > 30\n\t\t) {\n\t\t\tconst expiresAt = new Date(\n\t\t\t\tDate.now() + config.ttlSeconds * 1000,\n\t\t\t).toISOString();\n\t\t\tif (options.updateExisting) {\n\t\t\t\tplan.push({\n\t\t\t\t\tkind: \"update-branch-ttl\",\n\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\t\tbranchName,\n\t\t\t\t\texpiresAt,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconflicts.push({\n\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\tfield: \"ttl\",\n\t\t\t\t\tcurrent: remote.branch.expiresAt,\n\t\t\t\t\tdesired: expiresAt,\n\t\t\t\t\treason: \"Existing branch has a different TTL. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction computeDriftBetween(\n\tdesired: ComputeSettings,\n\tendpoint: NeonEndpointSnapshot,\n): {\n\tcurrent: Partial<ComputeSettings>;\n\tdesired: Partial<ComputeSettings>;\n} | null {\n\tconst currentDrift: Partial<ComputeSettings> = {};\n\tconst desiredDrift: Partial<ComputeSettings> = {};\n\tlet drift = false;\n\n\tif (\n\t\tdesired.autoscalingLimitMinCu !== undefined &&\n\t\tdesired.autoscalingLimitMinCu !== endpoint.autoscalingLimitMinCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;\n\t\tdesiredDrift.autoscalingLimitMinCu = desired.autoscalingLimitMinCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.autoscalingLimitMaxCu !== undefined &&\n\t\tdesired.autoscalingLimitMaxCu !== endpoint.autoscalingLimitMaxCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;\n\t\tdesiredDrift.autoscalingLimitMaxCu = desired.autoscalingLimitMaxCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.suspendTimeout !== undefined &&\n\t\tdesired.suspendTimeout !== endpoint.suspendTimeout\n\t) {\n\t\tcurrentDrift.suspendTimeout = endpoint.suspendTimeout;\n\t\tdesiredDrift.suspendTimeout = desired.suspendTimeout;\n\t\tdrift = true;\n\t}\n\treturn drift ? { current: currentDrift, desired: desiredDrift } : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"diff.js","names":[],"sources":["../../src/lib/diff.ts"],"sourcesContent":["import type {\n\tNeonBranchSnapshot,\n\tNeonBucketSnapshot,\n\tNeonEndpointSnapshot,\n\tNeonFunctionSnapshot,\n} from \"./neon-api.js\";\nimport type {\n\tBucketAccessLevel,\n\tComputeSettings,\n\tConflictReport,\n\tResolvedBranchConfig,\n\tResolvedFunctionConfig,\n} from \"./types.js\";\n\n/**\n * A planned action to perform a single mutation against the Neon API. The diff engine\n * produces a list of these for `pushConfig` to execute (or report).\n */\nexport type PlanStep =\n\t| {\n\t\t\tkind: \"update-branch-ttl\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\texpiresAt: string | null;\n\t }\n\t| {\n\t\t\tkind: \"update-branch-protected\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tprotected: boolean;\n\t }\n\t| {\n\t\t\tkind: \"update-endpoint\";\n\t\t\tprojectId: string;\n\t\t\tbranchName: string;\n\t\t\tendpointId: string;\n\t\t\tsettings: ComputeSettings;\n\t }\n\t| {\n\t\t\tkind: \"enable-auth\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName?: string;\n\t }\n\t| {\n\t\t\tkind: \"enable-data-api\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tdatabaseName: string;\n\t }\n\t| {\n\t\t\tkind: \"create-bucket\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tbucketName: string;\n\t\t\taccessLevel: BucketAccessLevel;\n\t }\n\t| {\n\t\t\t/**\n\t\t\t * Deploy code to a function. Planned for every desired function — deployments are\n\t\t\t * versioned and the newest becomes active, so a push ships the current source each\n\t\t\t * time. Neon has no separate \"create function\" endpoint: the first deployment to a\n\t\t\t * slug creates the function. `functionExists` therefore only drives whether this\n\t\t\t * surfaces as a `create` (first deploy) or an `update` (re-deploy).\n\t\t\t */\n\t\t\tkind: \"deploy-function\";\n\t\t\tprojectId: string;\n\t\t\tbranchId: string;\n\t\t\tbranchName: string;\n\t\t\tfn: ResolvedFunctionConfig;\n\t\t\t/** Whether the function already existed remotely when the plan was computed. */\n\t\t\tfunctionExists: boolean;\n\t };\n\nexport interface RemoteServiceState {\n\tdatabaseName: string;\n\tauthEnabled: boolean;\n\tdataApiEnabled: boolean;\n}\n\n/**\n * Snapshot of the branch's current Preview-feature state. Absent (`undefined`) when the\n * policy has no `preview` block — `pushConfig` only fetches this when needed.\n */\nexport interface RemotePreviewState {\n\tbuckets: NeonBucketSnapshot[];\n\tfunctions: NeonFunctionSnapshot[];\n}\n\nexport interface RemoteState {\n\tprojectId: string;\n\tbranch: NeonBranchSnapshot;\n\tendpoint?: NeonEndpointSnapshot;\n\tservices: RemoteServiceState;\n\tpreview?: RemotePreviewState;\n}\n\nexport interface DiffOptions {\n\t/**\n\t * Apply mutable drift on the selected branch as plan steps instead of conflicts.\n\t * Default: `false`.\n\t */\n\tupdateExisting: boolean;\n}\n\nexport interface DiffResult {\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\n/**\n * Diff desired branch policy against the selected remote branch. Pure function.\n */\nexport function diffConfig(\n\tconfig: ResolvedBranchConfig,\n\tremote: RemoteState,\n\toptions: DiffOptions,\n): DiffResult {\n\tconst conflicts: ConflictReport[] = [];\n\tconst plan: PlanStep[] = [];\n\tdiffBranchConfig({ config, remote, options, plan, conflicts });\n\tdiffServices({ config, remote, plan });\n\tdiffPreview({ config, remote, plan });\n\treturn { plan, conflicts };\n}\n\n/**\n * Plan Preview features (functions, buckets). Like {@link diffServices}, this is\n * **additive**: it creates desired buckets and (re-)deploys functions, but never deletes\n * them. Teardown is destructive, so it stays explicit/manual — matching the existing\n * auth / dataApi behaviour.\n *\n * The AI Gateway is intentionally NOT planned here: it is always available on a branch\n * (credential-gated, not per-branch provisioned), so `preview.aiGateway` produces no plan\n * step — it only drives the branch credential's `ai_gateway:invoke` scope and the gateway\n * env vars (`@neondatabase/env`). There is nothing to create, and nothing to probe.\n *\n * Functions are always (re-)deployed: deployments are versioned and the newest becomes\n * active, so each push ships the current source. There is no separate create step — Neon\n * creates the function on its first deployment — so a single `deploy-function` step is\n * emitted per desired function, carrying `functionExists` so the apply can report it as a\n * create (first deploy) or an update (re-deploy).\n */\nfunction diffPreview(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\tplan: PlanStep[];\n}): void {\n\tconst { config, remote, plan } = args;\n\tconst preview = config.preview;\n\tif (!preview) return;\n\t// `remote.preview` is only fetched when the policy has a preview block; treat a missing\n\t// snapshot as \"nothing exists yet\" so the diff is still well-defined.\n\tconst state: RemotePreviewState = remote.preview ?? {\n\t\tbuckets: [],\n\t\tfunctions: [],\n\t};\n\n\tfor (const bucket of preview.buckets) {\n\t\tif (state.buckets.some((b) => b.name === bucket.name)) continue;\n\t\tplan.push({\n\t\t\tkind: \"create-bucket\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tbucketName: bucket.name,\n\t\t\taccessLevel: bucket.access,\n\t\t});\n\t}\n\n\tfor (const fn of preview.functions) {\n\t\tconst exists = state.functions.some((f) => f.slug === fn.slug);\n\t\t// Neon creates the function on its first deployment (there is no separate create\n\t\t// endpoint), so always emit a single deploy step and let `functionExists` decide\n\t\t// whether it is reported as a create or an update.\n\t\tplan.push({\n\t\t\tkind: \"deploy-function\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tfn,\n\t\t\tfunctionExists: exists,\n\t\t});\n\t}\n}\n\n/**\n * Plan additive branch-scoped integrations. Disabling remains explicit/manual because\n * teardown is destructive.\n */\nfunction diffServices(args: {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\tplan: PlanStep[];\n}): void {\n\tconst { config, remote, plan } = args;\n\tconst state = remote.services;\n\tif (config.authEnabled && !state.authEnabled) {\n\t\tconst step: PlanStep = {\n\t\t\tkind: \"enable-auth\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t};\n\t\tif (state.databaseName) step.databaseName = state.databaseName;\n\t\tplan.push(step);\n\t}\n\tif (config.dataApiEnabled && !state.dataApiEnabled) {\n\t\tplan.push({\n\t\t\tkind: \"enable-data-api\",\n\t\t\tprojectId: remote.projectId,\n\t\t\tbranchId: remote.branch.id,\n\t\t\tbranchName: remote.branch.name,\n\t\t\tdatabaseName: state.databaseName,\n\t\t});\n\t}\n}\n\ninterface BranchConfigArgs {\n\tconfig: ResolvedBranchConfig;\n\tremote: RemoteState;\n\toptions: DiffOptions;\n\tplan: PlanStep[];\n\tconflicts: ConflictReport[];\n}\n\nfunction diffBranchConfig(args: BranchConfigArgs): void {\n\tconst { config, remote, options, plan, conflicts } = args;\n\tconst branchName = remote.branch.name;\n\tconst computeSettings = config.postgres?.computeSettings;\n\n\tif (computeSettings) {\n\t\tconst endpoint = remote.endpoint;\n\t\tif (!endpoint) {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"endpoint\",\n\t\t\t\tcurrent: undefined,\n\t\t\t\tdesired: computeSettings,\n\t\t\t\treason: \"Branch has no read-write endpoint; cannot apply compute settings.\",\n\t\t\t});\n\t\t} else {\n\t\t\tconst drift = computeDriftBetween(computeSettings, endpoint);\n\t\t\tif (drift) {\n\t\t\t\tif (options.updateExisting) {\n\t\t\t\t\tplan.push({\n\t\t\t\t\t\tkind: \"update-endpoint\",\n\t\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\t\tbranchName,\n\t\t\t\t\t\tendpointId: endpoint.id,\n\t\t\t\t\t\tsettings: computeSettings,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconflicts.push({\n\t\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\t\tfield: \"computeSettings\",\n\t\t\t\t\t\tcurrent: drift.current,\n\t\t\t\t\t\tdesired: drift.desired,\n\t\t\t\t\t\treason: \"Existing branch has different compute settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (\n\t\tconfig.protected !== undefined &&\n\t\tconfig.protected !== remote.branch.protected\n\t) {\n\t\tif (options.updateExisting) {\n\t\t\tplan.push({\n\t\t\t\tkind: \"update-branch-protected\",\n\t\t\t\tprojectId: remote.projectId,\n\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\tbranchName,\n\t\t\t\tprotected: config.protected,\n\t\t\t});\n\t\t} else {\n\t\t\tconflicts.push({\n\t\t\t\tkind: \"branch\",\n\t\t\t\tidentifier: branchName,\n\t\t\t\tfield: \"protected\",\n\t\t\t\tcurrent: remote.branch.protected,\n\t\t\t\tdesired: config.protected,\n\t\t\t\treason: \"Existing branch has a different `protected` flag. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t});\n\t\t}\n\t}\n\n\tif (config.ttlSeconds !== undefined) {\n\t\tconst current = remote.branch.expiresAt\n\t\t\t? Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.round(\n\t\t\t\t\t\t(Date.parse(remote.branch.expiresAt) - Date.now()) /\n\t\t\t\t\t\t\t1000,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t: undefined;\n\t\tif (\n\t\t\tcurrent === undefined ||\n\t\t\tMath.abs(current - config.ttlSeconds) > 30\n\t\t) {\n\t\t\tconst expiresAt = new Date(\n\t\t\t\tDate.now() + config.ttlSeconds * 1000,\n\t\t\t).toISOString();\n\t\t\tif (options.updateExisting) {\n\t\t\t\tplan.push({\n\t\t\t\t\tkind: \"update-branch-ttl\",\n\t\t\t\t\tprojectId: remote.projectId,\n\t\t\t\t\tbranchId: remote.branch.id,\n\t\t\t\t\tbranchName,\n\t\t\t\t\texpiresAt,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconflicts.push({\n\t\t\t\t\tkind: \"branch\",\n\t\t\t\t\tidentifier: branchName,\n\t\t\t\t\tfield: \"ttl\",\n\t\t\t\t\tcurrent: remote.branch.expiresAt,\n\t\t\t\t\tdesired: expiresAt,\n\t\t\t\t\treason: \"Existing branch has a different TTL. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction computeDriftBetween(\n\tdesired: ComputeSettings,\n\tendpoint: NeonEndpointSnapshot,\n): {\n\tcurrent: Partial<ComputeSettings>;\n\tdesired: Partial<ComputeSettings>;\n} | null {\n\tconst currentDrift: Partial<ComputeSettings> = {};\n\tconst desiredDrift: Partial<ComputeSettings> = {};\n\tlet drift = false;\n\n\tif (\n\t\tdesired.autoscalingLimitMinCu !== undefined &&\n\t\tdesired.autoscalingLimitMinCu !== endpoint.autoscalingLimitMinCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;\n\t\tdesiredDrift.autoscalingLimitMinCu = desired.autoscalingLimitMinCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.autoscalingLimitMaxCu !== undefined &&\n\t\tdesired.autoscalingLimitMaxCu !== endpoint.autoscalingLimitMaxCu\n\t) {\n\t\tcurrentDrift.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;\n\t\tdesiredDrift.autoscalingLimitMaxCu = desired.autoscalingLimitMaxCu;\n\t\tdrift = true;\n\t}\n\tif (\n\t\tdesired.suspendTimeout !== undefined &&\n\t\tdesired.suspendTimeout !== endpoint.suspendTimeout\n\t) {\n\t\tcurrentDrift.suspendTimeout = endpoint.suspendTimeout;\n\t\tdesiredDrift.suspendTimeout = desired.suspendTimeout;\n\t\tdrift = true;\n\t}\n\treturn drift ? { current: currentDrift, desired: desiredDrift } : null;\n}\n"],"mappings":";;;;AAsHA,SAAgB,WACf,QACA,QACA,SACa;CACb,MAAM,YAA8B,CAAC;CACrC,MAAM,OAAmB,CAAC;CAC1B,iBAAiB;EAAE;EAAQ;EAAQ;EAAS;EAAM;CAAU,CAAC;CAC7D,aAAa;EAAE;EAAQ;EAAQ;CAAK,CAAC;CACrC,YAAY;EAAE;EAAQ;EAAQ;CAAK,CAAC;CACpC,OAAO;EAAE;EAAM;CAAU;AAC1B;;;;;;;;;;;;;;;;;;AAmBA,SAAS,YAAY,MAIZ;CACR,MAAM,EAAE,QAAQ,QAAQ,SAAS;CACjC,MAAM,UAAU,OAAO;CACvB,IAAI,CAAC,SAAS;CAGd,MAAM,QAA4B,OAAO,WAAW;EACnD,SAAS,CAAC;EACV,WAAW,CAAC;CACb;CAEA,KAAK,MAAM,UAAU,QAAQ,SAAS;EACrC,IAAI,MAAM,QAAQ,MAAM,MAAM,EAAE,SAAS,OAAO,IAAI,GAAG;EACvD,KAAK,KAAK;GACT,MAAM;GACN,WAAW,OAAO;GAClB,UAAU,OAAO,OAAO;GACxB,YAAY,OAAO,OAAO;GAC1B,YAAY,OAAO;GACnB,aAAa,OAAO;EACrB,CAAC;CACF;CAEA,KAAK,MAAM,MAAM,QAAQ,WAAW;EACnC,MAAM,SAAS,MAAM,UAAU,MAAM,MAAM,EAAE,SAAS,GAAG,IAAI;EAI7D,KAAK,KAAK;GACT,MAAM;GACN,WAAW,OAAO;GAClB,UAAU,OAAO,OAAO;GACxB,YAAY,OAAO,OAAO;GAC1B;GACA,gBAAgB;EACjB,CAAC;CACF;AACD;;;;;AAMA,SAAS,aAAa,MAIb;CACR,MAAM,EAAE,QAAQ,QAAQ,SAAS;CACjC,MAAM,QAAQ,OAAO;CACrB,IAAI,OAAO,eAAe,CAAC,MAAM,aAAa;EAC7C,MAAM,OAAiB;GACtB,MAAM;GACN,WAAW,OAAO;GAClB,UAAU,OAAO,OAAO;GACxB,YAAY,OAAO,OAAO;EAC3B;EACA,IAAI,MAAM,cAAc,KAAK,eAAe,MAAM;EAClD,KAAK,KAAK,IAAI;CACf;CACA,IAAI,OAAO,kBAAkB,CAAC,MAAM,gBACnC,KAAK,KAAK;EACT,MAAM;EACN,WAAW,OAAO;EAClB,UAAU,OAAO,OAAO;EACxB,YAAY,OAAO,OAAO;EAC1B,cAAc,MAAM;CACrB,CAAC;AAEH;AAUA,SAAS,iBAAiB,MAA8B;CACvD,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,cAAc;CACrD,MAAM,aAAa,OAAO,OAAO;CACjC,MAAM,kBAAkB,OAAO,UAAU;CAEzC,IAAI,iBAAiB;EACpB,MAAM,WAAW,OAAO;EACxB,IAAI,CAAC,UACJ,UAAU,KAAK;GACd,MAAM;GACN,YAAY;GACZ,OAAO;GACP,SAAS,KAAA;GACT,SAAS;GACT,QAAQ;EACT,CAAC;OACK;GACN,MAAM,QAAQ,oBAAoB,iBAAiB,QAAQ;GAC3D,IAAI,OACH,IAAI,QAAQ,gBACX,KAAK,KAAK;IACT,MAAM;IACN,WAAW,OAAO;IAClB;IACA,YAAY,SAAS;IACrB,UAAU;GACX,CAAC;QAED,UAAU,KAAK;IACd,MAAM;IACN,YAAY;IACZ,OAAO;IACP,SAAS,MAAM;IACf,SAAS,MAAM;IACf,QAAQ;GACT,CAAC;EAGJ;CACD;CAEA,IACC,OAAO,cAAc,KAAA,KACrB,OAAO,cAAc,OAAO,OAAO,WAEnC,IAAI,QAAQ,gBACX,KAAK,KAAK;EACT,MAAM;EACN,WAAW,OAAO;EAClB,UAAU,OAAO,OAAO;EACxB;EACA,WAAW,OAAO;CACnB,CAAC;MAED,UAAU,KAAK;EACd,MAAM;EACN,YAAY;EACZ,OAAO;EACP,SAAS,OAAO,OAAO;EACvB,SAAS,OAAO;EAChB,QAAQ;CACT,CAAC;CAIH,IAAI,OAAO,eAAe,KAAA,GAAW;EACpC,MAAM,UAAU,OAAO,OAAO,YAC3B,KAAK,IACL,GACA,KAAK,OACH,KAAK,MAAM,OAAO,OAAO,SAAS,IAAI,KAAK,IAAI,KAC/C,GACF,CACD,IACC,KAAA;EACH,IACC,YAAY,KAAA,KACZ,KAAK,IAAI,UAAU,OAAO,UAAU,IAAI,IACvC;GACD,MAAM,YAAY,IAAI,KACrB,KAAK,IAAI,IAAI,OAAO,aAAa,GAClC,CAAC,CAAC,YAAY;GACd,IAAI,QAAQ,gBACX,KAAK,KAAK;IACT,MAAM;IACN,WAAW,OAAO;IAClB,UAAU,OAAO,OAAO;IACxB;IACA;GACD,CAAC;QAED,UAAU,KAAK;IACd,MAAM;IACN,YAAY;IACZ,OAAO;IACP,SAAS,OAAO,OAAO;IACvB,SAAS;IACT,QAAQ;GACT,CAAC;EAEH;CACD;AACD;AAEA,SAAS,oBACR,SACA,UAIQ;CACR,MAAM,eAAyC,CAAC;CAChD,MAAM,eAAyC,CAAC;CAChD,IAAI,QAAQ;CAEZ,IACC,QAAQ,0BAA0B,KAAA,KAClC,QAAQ,0BAA0B,SAAS,uBAC1C;EACD,aAAa,wBAAwB,SAAS;EAC9C,aAAa,wBAAwB,QAAQ;EAC7C,QAAQ;CACT;CACA,IACC,QAAQ,0BAA0B,KAAA,KAClC,QAAQ,0BAA0B,SAAS,uBAC1C;EACD,aAAa,wBAAwB,SAAS;EAC9C,aAAa,wBAAwB,QAAQ;EAC7C,QAAQ;CACT;CACA,IACC,QAAQ,mBAAmB,KAAA,KAC3B,QAAQ,mBAAmB,SAAS,gBACnC;EACD,aAAa,iBAAiB,SAAS;EACvC,aAAa,iBAAiB,QAAQ;EACtC,QAAQ;CACT;CACA,OAAO,QAAQ;EAAE,SAAS;EAAc,SAAS;CAAa,IAAI;AACnE"}
@@ -1 +1 @@
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
+ {"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":"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"}
@@ -63,6 +63,16 @@ declare class PlatformError extends Error {
63
63
  details?: Record<string, unknown>;
64
64
  });
65
65
  }
66
+ /**
67
+ * Structural check for a {@link PlatformError}.
68
+ *
69
+ * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a
70
+ * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this
71
+ * package, so a `PlatformError` thrown while evaluating it has a different class identity
72
+ * than ours and fails `instanceof` — but its stable string `code` (always prefixed
73
+ * `PLATFORM_`) survives the boundary intact, which is what we match on here.
74
+ */
75
+ declare function isPlatformError(value: unknown): value is PlatformError;
66
76
  /**
67
77
  * Append a "report-a-bug" footer to an error message. Used only on truly unreachable
68
78
  * internal errors — never on user-facing validation / configuration errors where the user
@@ -126,5 +136,5 @@ declare class ConfigLoadError extends PlatformError {
126
136
  });
127
137
  }
128
138
  //#endregion
129
- export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter };
139
+ export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter, isPlatformError };
130
140
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","names":[],"sources":["../../src/lib/errors.ts"],"mappings":";;;;;;AAuBA;AAsBA;;;;AAAiE;AAYjE;;;;;;AAAwC;AAqBxC;AAUA;AAoBA;AAeA;;;AAIiC,cAxGpB,SAwGoB,EAAA;WAJM,aAAA,EAAA,yBAAA;EAAa,SAAA,cAAA,EAAA,2BAAA;EAqDvC,SAAA,cAAiB,EAAA,0BAAqB;EA8BtC,SAAA,YAAgB,EAAA,wBAAqB;;;;;;;;;;;;;;;;;;KAjKtC,SAAA,WAAoB,wBAAwB;;;;;;;;;cAY3C,aAAA,SAAsB,KAAA;;;oBAGhB,SAAS;;;cAKa;;;;;;;;iBAazB,eAAA,CAAA;;;;;;;cAUH,qBAAA,SAA8B,aAAa;;;;;;;;;;;;cAoB3C,mBAAA,SAA4B,aAAa;;;;;;;;;;;cAezC,iBAAA,SAA0B,aAAA;;+BAET;kCAEG;;;;;;;;;cAiDpB,gBAAA,SAAyB,aAAa;;;;;;;;;cA8BtC,eAAA,SAAwB,aAAa"}
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../../src/lib/errors.ts"],"mappings":";;;;;;AAuBA;AAsBA;;;;AAAiE;AAYjE;;;;;;AAAwC;AAyBxC;AAaA;AAUA;AAoBA;AAeA;;AAE8B,cAvHjB,SAuHiB,EAAA;WAEG,aAAA,EAAA,yBAAA;WAJM,cAAA,EAAA,2BAAA;EAAa,SAAA,cAAA,EAAA,0BAAA;EAqDvC,SAAA,YAAiB,EAAA,wBAAqB;EA8BtC,SAAA,WAAgB,EAAA,uBAAqB;;;;;;;;;;;;;;;;;KAlLtC,SAAA,WAAoB,wBAAwB;;;;;;;;;cAY3C,aAAA,SAAsB,KAAA;;;oBAGhB,SAAS;;;cAKa;;;;;;;;;;;;iBAiBzB,eAAA,2BAA0C;;;;;;iBAa1C,eAAA,CAAA;;;;;;;cAUH,qBAAA,SAA8B,aAAa;;;;;;;;;;;;cAoB3C,mBAAA,SAA4B,aAAa;;;;;;;;;;;cAezC,iBAAA,SAA0B,aAAA;;+BAET;kCAEG;;;;;;;;;cAiDpB,gBAAA,SAAyB,aAAa;;;;;;;;;cA8BtC,eAAA,SAAwB,aAAa"}
@@ -62,6 +62,22 @@ var PlatformError = class extends Error {
62
62
  }
63
63
  };
64
64
  /**
65
+ * Structural check for a {@link PlatformError}.
66
+ *
67
+ * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a
68
+ * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this
69
+ * package, so a `PlatformError` thrown while evaluating it has a different class identity
70
+ * than ours and fails `instanceof` — but its stable string `code` (always prefixed
71
+ * `PLATFORM_`) survives the boundary intact, which is what we match on here.
72
+ */
73
+ function isPlatformError(value) {
74
+ if (value instanceof PlatformError) return true;
75
+ if (typeof value !== "object" || value === null) return false;
76
+ if (!("code" in value)) return false;
77
+ const { code } = value;
78
+ return typeof code === "string" && code.startsWith("PLATFORM_");
79
+ }
80
+ /**
65
81
  * Append a "report-a-bug" footer to an error message. Used only on truly unreachable
66
82
  * internal errors — never on user-facing validation / configuration errors where the user
67
83
  * is supposed to fix something on their end.
@@ -164,6 +180,6 @@ function formatValue(value) {
164
180
  return String(value);
165
181
  }
166
182
  //#endregion
167
- export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter };
183
+ export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PlatformError, PushAbortedError, PushConflictError, bugReportFooter, isPlatformError };
168
184
 
169
185
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/lib/errors.ts"],"sourcesContent":["import type { ConflictReport } from \"./types.js\";\n\n/**\n * Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on\n * these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of\n * matching on free-text messages.\n *\n * Grouped by source:\n * - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.\n * - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.\n * - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not\n * opt in to apply.\n * - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.\n * - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.\n * - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent\n * branch.\n * - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /\n * `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /\n * `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.\n * - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).\n * - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;\n * if you see one, please open an issue.\n */\nexport const ErrorCode = {\n\tInvalidConfig: \"PLATFORM_INVALID_CONFIG\",\n\tEnvNotInjected: \"PLATFORM_ENV_NOT_INJECTED\",\n\tMissingContext: \"PLATFORM_MISSING_CONTEXT\",\n\tPushConflict: \"PLATFORM_PUSH_CONFLICT\",\n\tPushAborted: \"PLATFORM_PUSH_ABORTED\",\n\tConfigLoadFailed: \"PLATFORM_CONFIG_LOAD_FAILED\",\n\tMissingApiKey: \"PLATFORM_MISSING_API_KEY\",\n\tAmbiguousBranchAuth: \"PLATFORM_AMBIGUOUS_BRANCH_AUTH\",\n\tBranchNotFound: \"PLATFORM_BRANCH_NOT_FOUND\",\n\tFeatureUnavailable: \"PLATFORM_FEATURE_UNAVAILABLE\",\n\tMissingParentBranch: \"PLATFORM_MISSING_PARENT_BRANCH\",\n\tUnauthorized: \"PLATFORM_UNAUTHORIZED\",\n\tForbidden: \"PLATFORM_FORBIDDEN\",\n\tNotFound: \"PLATFORM_NOT_FOUND\",\n\tConflict: \"PLATFORM_CONFLICT\",\n\tRateLimited: \"PLATFORM_RATE_LIMITED\",\n\tLocked: \"PLATFORM_LOCKED\",\n\tServerError: \"PLATFORM_SERVER_ERROR\",\n\tNetworkError: \"PLATFORM_NETWORK_ERROR\",\n\tInternalError: \"PLATFORM_INTERNAL_ERROR\",\n} as const;\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nconst ISSUE_URL = \"https://github.com/neondatabase/neon-pkgs/issues/new\";\n\n/**\n * Base class for all errors thrown by `@neondatabase/config`. Always extend this so callers\n * can catch every package-thrown error with a single `instanceof` check.\n *\n * Optional `details` carries structured context that the CLI prints under `--debug` and\n * that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,\n * `details.requestId` for Neon API failures).\n */\nexport class PlatformError extends Error {\n\toverride readonly name: string = \"PlatformError\";\n\treadonly code: string;\n\treadonly details: Readonly<Record<string, unknown>>;\n\n\tconstructor(\n\t\tcode: string,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown; details?: Record<string, unknown> },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t\tthis.details = Object.freeze({ ...(options?.details ?? {}) });\n\t}\n}\n\n/**\n * Append a \"report-a-bug\" footer to an error message. Used only on truly unreachable\n * internal errors — never on user-facing validation / configuration errors where the user\n * is supposed to fix something on their end.\n */\nexport function bugReportFooter(): string {\n\treturn `\\nThis indicates a bug in @neondatabase/config. Please file an issue: ${ISSUE_URL}`;\n}\n\n/**\n * Thrown by {@link defineConfig} when the user-provided configuration object is invalid.\n *\n * The class collects every validation failure rather than throwing on the first one so that\n * users get a complete picture of what is wrong with their `neon.ts`.\n */\nexport class ConfigValidationError extends PlatformError {\n\toverride readonly name = \"ConfigValidationError\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(\n\t\t\t\"PLATFORM_INVALID_CONFIG\",\n\t\t\t`Invalid Neon platform config:\\n - ${issues.join(\"\\n - \")}`,\n\t\t);\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Thrown when the package cannot resolve which Neon project to operate on.\n *\n * Per the package's read-only-filesystem contract, we never create a `.neon` context file;\n * callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file\n * (`.neon/project.json` or neonctl's `.neon`).\n */\nexport class MissingContextError extends PlatformError {\n\toverride readonly name = \"MissingContextError\";\n\n\tconstructor(message: string) {\n\t\tsuper(\"PLATFORM_MISSING_CONTEXT\", message);\n\t}\n}\n\n/**\n * Thrown by {@link pushConfig} when it detects differences between the local config and\n * the remote project that the caller hasn't opted in to apply.\n *\n * The message lists every conflict with both the current and desired value plus a\n * per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.\n */\nexport class PushConflictError extends PlatformError {\n\toverride readonly name = \"PushConflictError\";\n\treadonly conflicts: readonly ConflictReport[];\n\n\tconstructor(conflicts: readonly ConflictReport[]) {\n\t\tconst lines: string[] = [\n\t\t\t\"pushConfig refused to apply: local config conflicts with remote state.\",\n\t\t\t\"\",\n\t\t];\n\t\tfor (const c of conflicts) {\n\t\t\tlines.push(\n\t\t\t\t` - [${c.kind}:${c.identifier}] ${c.field}: ${c.reason}`,\n\t\t\t\t` current : ${formatValue(c.current)}`,\n\t\t\t\t` desired : ${formatValue(c.desired)}`,\n\t\t\t\t` fix : ${suggestFix(c)}`,\n\t\t\t);\n\t\t}\n\t\tconst hasMutable = conflicts.some((c) => !isImmutableConflict(c));\n\t\tlines.push(\"\");\n\t\tif (hasMutable) {\n\t\t\tlines.push(\n\t\t\t\t\"For mutable conflicts, pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\",\n\t\t\t);\n\t\t}\n\n\t\tsuper(\"PLATFORM_PUSH_CONFLICT\", lines.join(\"\\n\"), {\n\t\t\tdetails: { conflicts: conflicts.map((c) => ({ ...c })) },\n\t\t});\n\t\tthis.conflicts = conflicts;\n\t}\n}\n\nfunction isImmutableConflict(_c: ConflictReport): boolean {\n\treturn false;\n}\n\nfunction suggestFix(c: ConflictReport): string {\n\tif (isImmutableConflict(c)) {\n\t\treturn \"immutable on Neon — recreate the project, or change your `neon.ts` to match the remote.\";\n\t}\n\tif (c.kind === \"branch\" && c.field === \"parent\") {\n\t\treturn \"create the parent branch on Neon first, or change the `parent` reference to an existing branch.\";\n\t}\n\treturn \"pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\";\n}\n\n/**\n * Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a\n * push that requires confirmation (protected branch and/or mutable drift overriding\n * existing remote settings).\n *\n * The CLI maps this to a non-zero exit so users see \"aborted\" rather than a stack trace.\n */\nexport class PushAbortedError extends PlatformError {\n\toverride readonly name = \"PushAbortedError\";\n\treadonly branchName: string;\n\treadonly reasons: readonly (\"protected-branch\" | \"override-updates\")[];\n\n\tconstructor(\n\t\tbranchName: string,\n\t\treasons: readonly (\"protected-branch\" | \"override-updates\")[],\n\t) {\n\t\tsuper(\n\t\t\t\"PLATFORM_PUSH_ABORTED\",\n\t\t\t[\n\t\t\t\t`Aborted push to branch ${JSON.stringify(branchName)}.`,\n\t\t\t\treasons.length > 0\n\t\t\t\t\t? `Reason${reasons.length === 1 ? \"\" : \"s\"}: ${reasons.join(\", \")}.`\n\t\t\t\t\t: undefined,\n\t\t\t\t\"Re-run with `--update-existing` (override existing settings) or `--allow-protected-branch` (push to a protected branch) to skip the prompt.\",\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\" \"),\n\t\t\t{ details: { branchName, reasons: [...reasons] } },\n\t\t);\n\t\tthis.branchName = branchName;\n\t\tthis.reasons = reasons;\n\t}\n}\n\n/**\n * Thrown when the SDK fails to find or load a `neon.ts` config file.\n */\nexport class ConfigLoadError extends PlatformError {\n\toverride readonly name = \"ConfigLoadError\";\n\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(\"PLATFORM_CONFIG_LOAD_FAILED\", message, options);\n\t}\n}\n\nfunction formatValue(value: unknown): string {\n\tif (value === undefined) return \"<unset>\";\n\tif (typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"object\" && value !== null)\n\t\treturn JSON.stringify(value);\n\treturn String(value);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,YAAY;CACxB,eAAe;CACf,gBAAgB;CAChB,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,kBAAkB;CAClB,eAAe;CACf,qBAAqB;CACrB,gBAAgB;CAChB,oBAAoB;CACpB,qBAAqB;CACrB,cAAc;CACd,WAAW;CACX,UAAU;CACV,UAAU;CACV,aAAa;CACb,QAAQ;CACR,aAAa;CACb,cAAc;CACd,eAAe;AAChB;AAGA,MAAM,YAAY;;;;;;;;;AAUlB,IAAa,gBAAb,cAAmC,MAAM;CACxC,OAAiC;CACjC;CACA;CAEA,YACC,MACA,SACA,SACC;EACD,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,UAAU,OAAO,OAAO,EAAE,GAAI,SAAS,WAAW,CAAC,EAAG,CAAC;CAC7D;AACD;;;;;;AAOA,SAAgB,kBAA0B;CACzC,OAAO,yEAAyE;AACjF;;;;;;;AAQA,IAAa,wBAAb,cAA2C,cAAc;CACxD,OAAyB;CACzB;CAEA,YAAY,QAA2B;EACtC,MACC,2BACA,sCAAsC,OAAO,KAAK,QAAQ,GAC3D;EACA,KAAK,SAAS;CACf;AACD;;;;;;;;AASA,IAAa,sBAAb,cAAyC,cAAc;CACtD,OAAyB;CAEzB,YAAY,SAAiB;EAC5B,MAAM,4BAA4B,OAAO;CAC1C;AACD;;;;;;;;AASA,IAAa,oBAAb,cAAuC,cAAc;CACpD,OAAyB;CACzB;CAEA,YAAY,WAAsC;EACjD,MAAM,QAAkB,CACvB,0EACA,EACD;EACA,KAAK,MAAM,KAAK,WACf,MAAM,KACL,QAAQ,EAAE,KAAK,GAAG,EAAE,WAAW,IAAI,EAAE,MAAM,IAAI,EAAE,UACjD,mBAAmB,YAAY,EAAE,OAAO,KACxC,mBAAmB,YAAY,EAAE,OAAO,KACxC,mBAAmB,WAAW,CAAC,GAChC;EAED,MAAM,aAAa,UAAU,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;EAChE,MAAM,KAAK,EAAE;EACb,IAAI,YACH,MAAM,KACL,gGACD;EAGD,MAAM,0BAA0B,MAAM,KAAK,IAAI,GAAG,EACjD,SAAS,EAAE,WAAW,UAAU,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,EACxD,CAAC;EACD,KAAK,YAAY;CAClB;AACD;AAEA,SAAS,oBAAoB,IAA6B;CACzD,OAAO;AACR;AAEA,SAAS,WAAW,GAA2B;CAC9C,IAAI,oBAAoB,CAAC,GACxB,OAAO;CAER,IAAI,EAAE,SAAS,YAAY,EAAE,UAAU,UACtC,OAAO;CAER,OAAO;AACR;;;;;;;;AASA,IAAa,mBAAb,cAAsC,cAAc;CACnD,OAAyB;CACzB;CACA;CAEA,YACC,YACA,SACC;EACD,MACC,yBACA;GACC,0BAA0B,KAAK,UAAU,UAAU,EAAE;GACrD,QAAQ,SAAS,IACd,SAAS,QAAQ,WAAW,IAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,KAChE,KAAA;GACH;EACD,EACE,OAAO,OAAO,EACd,KAAK,GAAG,GACV,EAAE,SAAS;GAAE;GAAY,SAAS,CAAC,GAAG,OAAO;EAAE,EAAE,CAClD;EACA,KAAK,aAAa;EAClB,KAAK,UAAU;CAChB;AACD;;;;AAKA,IAAa,kBAAb,cAAqC,cAAc;CAClD,OAAyB;CAEzB,YAAY,SAAiB,SAA+B;EAC3D,MAAM,+BAA+B,SAAS,OAAO;CACtD;AACD;AAEA,SAAS,YAAY,OAAwB;CAC5C,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI,OAAO,UAAU,UAAU,OAAO,KAAK,UAAU,KAAK;CAC1D,IAAI,OAAO,UAAU,YAAY,UAAU,MAC1C,OAAO,KAAK,UAAU,KAAK;CAC5B,OAAO,OAAO,KAAK;AACpB"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/lib/errors.ts"],"sourcesContent":["import type { ConflictReport } from \"./types.js\";\n\n/**\n * Every code a {@link PlatformError} can carry. Stable identifiers — consumers can rely on\n * these for `instanceof PlatformError && err.code === ErrorCode.…` style checks instead of\n * matching on free-text messages.\n *\n * Grouped by source:\n * - `PLATFORM_INVALID_CONFIG` — `defineConfig` / `configSchema` rejected the input.\n * - `PLATFORM_MISSING_CONTEXT` — no project / branch context could be resolved.\n * - `PLATFORM_PUSH_CONFLICT` — local config conflicts with remote and the caller did not\n * opt in to apply.\n * - `PLATFORM_CONFIG_LOAD_FAILED` — `neon.ts` could not be found or evaluated.\n * - `PLATFORM_MISSING_API_KEY` — no `NEON_API_KEY` and no explicit `apiKey` was provided.\n * - `PLATFORM_MISSING_PARENT_BRANCH` — push tried to create a child of a non-existent\n * branch.\n * - `PLATFORM_UNAUTHORIZED` / `PLATFORM_FORBIDDEN` / `PLATFORM_NOT_FOUND` /\n * `PLATFORM_CONFLICT` / `PLATFORM_RATE_LIMITED` / `PLATFORM_LOCKED` /\n * `PLATFORM_SERVER_ERROR` — wrappings of Neon HTTP failures.\n * - `PLATFORM_NETWORK_ERROR` — transport-level failure (no HTTP response at all).\n * - `PLATFORM_INTERNAL_ERROR` — invariant violations. Should never happen in production;\n * if you see one, please open an issue.\n */\nexport const ErrorCode = {\n\tInvalidConfig: \"PLATFORM_INVALID_CONFIG\",\n\tEnvNotInjected: \"PLATFORM_ENV_NOT_INJECTED\",\n\tMissingContext: \"PLATFORM_MISSING_CONTEXT\",\n\tPushConflict: \"PLATFORM_PUSH_CONFLICT\",\n\tPushAborted: \"PLATFORM_PUSH_ABORTED\",\n\tConfigLoadFailed: \"PLATFORM_CONFIG_LOAD_FAILED\",\n\tMissingApiKey: \"PLATFORM_MISSING_API_KEY\",\n\tAmbiguousBranchAuth: \"PLATFORM_AMBIGUOUS_BRANCH_AUTH\",\n\tBranchNotFound: \"PLATFORM_BRANCH_NOT_FOUND\",\n\tFeatureUnavailable: \"PLATFORM_FEATURE_UNAVAILABLE\",\n\tMissingParentBranch: \"PLATFORM_MISSING_PARENT_BRANCH\",\n\tUnauthorized: \"PLATFORM_UNAUTHORIZED\",\n\tForbidden: \"PLATFORM_FORBIDDEN\",\n\tNotFound: \"PLATFORM_NOT_FOUND\",\n\tConflict: \"PLATFORM_CONFLICT\",\n\tRateLimited: \"PLATFORM_RATE_LIMITED\",\n\tLocked: \"PLATFORM_LOCKED\",\n\tServerError: \"PLATFORM_SERVER_ERROR\",\n\tNetworkError: \"PLATFORM_NETWORK_ERROR\",\n\tInternalError: \"PLATFORM_INTERNAL_ERROR\",\n} as const;\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nconst ISSUE_URL = \"https://github.com/neondatabase/neon-pkgs/issues/new\";\n\n/**\n * Base class for all errors thrown by `@neondatabase/config`. Always extend this so callers\n * can catch every package-thrown error with a single `instanceof` check.\n *\n * Optional `details` carries structured context that the CLI prints under `--debug` and\n * that programmatic consumers can read (e.g. `details.status` for HTTP wrappings,\n * `details.requestId` for Neon API failures).\n */\nexport class PlatformError extends Error {\n\toverride readonly name: string = \"PlatformError\";\n\treadonly code: string;\n\treadonly details: Readonly<Record<string, unknown>>;\n\n\tconstructor(\n\t\tcode: string,\n\t\tmessage: string,\n\t\toptions?: { cause?: unknown; details?: Record<string, unknown> },\n\t) {\n\t\tsuper(message, options);\n\t\tthis.code = code;\n\t\tthis.details = Object.freeze({ ...(options?.details ?? {}) });\n\t}\n}\n\n/**\n * Structural check for a {@link PlatformError}.\n *\n * Prefer this over a bare `instanceof PlatformError` whenever the error may have crossed a\n * module-realm boundary. A `neon.ts` loaded through jiti imports its *own* copy of this\n * package, so a `PlatformError` thrown while evaluating it has a different class identity\n * than ours and fails `instanceof` — but its stable string `code` (always prefixed\n * `PLATFORM_`) survives the boundary intact, which is what we match on here.\n */\nexport function isPlatformError(value: unknown): value is PlatformError {\n\tif (value instanceof PlatformError) return true;\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"code\" in value)) return false;\n\tconst { code } = value;\n\treturn typeof code === \"string\" && code.startsWith(\"PLATFORM_\");\n}\n\n/**\n * Append a \"report-a-bug\" footer to an error message. Used only on truly unreachable\n * internal errors — never on user-facing validation / configuration errors where the user\n * is supposed to fix something on their end.\n */\nexport function bugReportFooter(): string {\n\treturn `\\nThis indicates a bug in @neondatabase/config. Please file an issue: ${ISSUE_URL}`;\n}\n\n/**\n * Thrown by {@link defineConfig} when the user-provided configuration object is invalid.\n *\n * The class collects every validation failure rather than throwing on the first one so that\n * users get a complete picture of what is wrong with their `neon.ts`.\n */\nexport class ConfigValidationError extends PlatformError {\n\toverride readonly name = \"ConfigValidationError\";\n\treadonly issues: readonly string[];\n\n\tconstructor(issues: readonly string[]) {\n\t\tsuper(\n\t\t\t\"PLATFORM_INVALID_CONFIG\",\n\t\t\t`Invalid Neon platform config:\\n - ${issues.join(\"\\n - \")}`,\n\t\t);\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * Thrown when the package cannot resolve which Neon project to operate on.\n *\n * Per the package's read-only-filesystem contract, we never create a `.neon` context file;\n * callers must either pass `projectId`/`orgId` explicitly or rely on an existing context file\n * (`.neon/project.json` or neonctl's `.neon`).\n */\nexport class MissingContextError extends PlatformError {\n\toverride readonly name = \"MissingContextError\";\n\n\tconstructor(message: string) {\n\t\tsuper(\"PLATFORM_MISSING_CONTEXT\", message);\n\t}\n}\n\n/**\n * Thrown by {@link pushConfig} when it detects differences between the local config and\n * the remote project that the caller hasn't opted in to apply.\n *\n * The message lists every conflict with both the current and desired value plus a\n * per-conflict hint. Mutable branch drift is applied by passing `updateExisting: true`.\n */\nexport class PushConflictError extends PlatformError {\n\toverride readonly name = \"PushConflictError\";\n\treadonly conflicts: readonly ConflictReport[];\n\n\tconstructor(conflicts: readonly ConflictReport[]) {\n\t\tconst lines: string[] = [\n\t\t\t\"pushConfig refused to apply: local config conflicts with remote state.\",\n\t\t\t\"\",\n\t\t];\n\t\tfor (const c of conflicts) {\n\t\t\tlines.push(\n\t\t\t\t` - [${c.kind}:${c.identifier}] ${c.field}: ${c.reason}`,\n\t\t\t\t` current : ${formatValue(c.current)}`,\n\t\t\t\t` desired : ${formatValue(c.desired)}`,\n\t\t\t\t` fix : ${suggestFix(c)}`,\n\t\t\t);\n\t\t}\n\t\tconst hasMutable = conflicts.some((c) => !isImmutableConflict(c));\n\t\tlines.push(\"\");\n\t\tif (hasMutable) {\n\t\t\tlines.push(\n\t\t\t\t\"For mutable conflicts, pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\",\n\t\t\t);\n\t\t}\n\n\t\tsuper(\"PLATFORM_PUSH_CONFLICT\", lines.join(\"\\n\"), {\n\t\t\tdetails: { conflicts: conflicts.map((c) => ({ ...c })) },\n\t\t});\n\t\tthis.conflicts = conflicts;\n\t}\n}\n\nfunction isImmutableConflict(_c: ConflictReport): boolean {\n\treturn false;\n}\n\nfunction suggestFix(c: ConflictReport): string {\n\tif (isImmutableConflict(c)) {\n\t\treturn \"immutable on Neon — recreate the project, or change your `neon.ts` to match the remote.\";\n\t}\n\tif (c.kind === \"branch\" && c.field === \"parent\") {\n\t\treturn \"create the parent branch on Neon first, or change the `parent` reference to an existing branch.\";\n\t}\n\treturn \"pass `updateExisting: true` (SDK) / `--update-existing` (CLI) to apply.\";\n}\n\n/**\n * Thrown by {@link pushConfig} when the caller-supplied `confirm` callback declines a\n * push that requires confirmation (protected branch and/or mutable drift overriding\n * existing remote settings).\n *\n * The CLI maps this to a non-zero exit so users see \"aborted\" rather than a stack trace.\n */\nexport class PushAbortedError extends PlatformError {\n\toverride readonly name = \"PushAbortedError\";\n\treadonly branchName: string;\n\treadonly reasons: readonly (\"protected-branch\" | \"override-updates\")[];\n\n\tconstructor(\n\t\tbranchName: string,\n\t\treasons: readonly (\"protected-branch\" | \"override-updates\")[],\n\t) {\n\t\tsuper(\n\t\t\t\"PLATFORM_PUSH_ABORTED\",\n\t\t\t[\n\t\t\t\t`Aborted push to branch ${JSON.stringify(branchName)}.`,\n\t\t\t\treasons.length > 0\n\t\t\t\t\t? `Reason${reasons.length === 1 ? \"\" : \"s\"}: ${reasons.join(\", \")}.`\n\t\t\t\t\t: undefined,\n\t\t\t\t\"Re-run with `--update-existing` (override existing settings) or `--allow-protected-branch` (push to a protected branch) to skip the prompt.\",\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\" \"),\n\t\t\t{ details: { branchName, reasons: [...reasons] } },\n\t\t);\n\t\tthis.branchName = branchName;\n\t\tthis.reasons = reasons;\n\t}\n}\n\n/**\n * Thrown when the SDK fails to find or load a `neon.ts` config file.\n */\nexport class ConfigLoadError extends PlatformError {\n\toverride readonly name = \"ConfigLoadError\";\n\n\tconstructor(message: string, options?: { cause?: unknown }) {\n\t\tsuper(\"PLATFORM_CONFIG_LOAD_FAILED\", message, options);\n\t}\n}\n\nfunction formatValue(value: unknown): string {\n\tif (value === undefined) return \"<unset>\";\n\tif (typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"object\" && value !== null)\n\t\treturn JSON.stringify(value);\n\treturn String(value);\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ConfigLoadError } from "./errors.js";
1
+ import { ConfigLoadError, isPlatformError } from "./errors.js";
2
2
  import { defineConfig } from "./define-config.js";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { dirname, isAbsolute, resolve } from "node:path";
@@ -42,9 +42,10 @@ async function loadConfigFromFile(options = {}) {
42
42
  try {
43
43
  mod = await importModule(resolvedPath);
44
44
  } catch (cause) {
45
+ if (isPlatformError(cause)) throw cause;
45
46
  throw new ConfigLoadError([
46
47
  `Failed to evaluate ${resolvedPath}.`,
47
- `Underlying error: ${cause?.message ?? String(cause)}`,
48
+ `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`,
48
49
  "This is usually a TypeScript syntax error, a missing dependency, or a runtime exception inside the config file. Run the file directly (e.g. `npx tsx neon.ts`) to reproduce."
49
50
  ].join("\n"), { cause });
50
51
  }