@neon/env 0.0.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ import { ErrorCode, PlatformError, createNeonApiFromOptions, deriveCredentialScopes, resolveConfig } from "@neon/config/v1";
2
+ import { z } from "zod";
3
+ //#region src/lib/env.ts
4
+ /**
5
+ * Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
6
+ * for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
7
+ * that talks to `process.env`).
8
+ *
9
+ * Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
10
+ * camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
11
+ * by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
12
+ * {@link dataApiEnvSchema}.
13
+ */
14
+ /**
15
+ * Neon's default branch owner role, created with every project. This is the role a
16
+ * `DATABASE_URL` should connect as.
17
+ */
18
+ const NEON_DEFAULT_OWNER_ROLE = "neondb_owner";
19
+ /**
20
+ * Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
21
+ * RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
22
+ * so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
23
+ * Data API (`neon config apply`) adds these next to the owner role, which is why a plain
24
+ * branch routinely reports more than one role.
25
+ */
26
+ const NEON_MANAGED_AUTH_ROLES = /* @__PURE__ */ new Set([
27
+ "authenticator",
28
+ "anonymous",
29
+ "authenticated"
30
+ ]);
31
+ const NEON_ENV_VAR_KEYS = {
32
+ /**
33
+ * Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
34
+ * Neon Functions runtime on every branch (including the default) by default. `env pull` /
35
+ * `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
36
+ */
37
+ branch: { name: "NEON_BRANCH" },
38
+ postgres: {
39
+ databaseUrl: "DATABASE_URL",
40
+ databaseUrlUnpooled: "DATABASE_URL_UNPOOLED"
41
+ },
42
+ auth: {
43
+ baseUrl: "NEON_AUTH_BASE_URL",
44
+ jwksUrl: "NEON_AUTH_JWKS_URL"
45
+ },
46
+ dataApi: { url: "NEON_DATA_API_URL" },
47
+ /**
48
+ * Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
49
+ * a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
50
+ * `region` is injected under the SDK-standard `AWS_REGION`.
51
+ */
52
+ storage: {
53
+ accessKeyId: "AWS_ACCESS_KEY_ID",
54
+ secretAccessKey: "AWS_SECRET_ACCESS_KEY",
55
+ endpoint: "AWS_ENDPOINT_URL_S3",
56
+ region: "AWS_REGION"
57
+ },
58
+ /**
59
+ * AI Gateway (Preview). Mapped onto the OpenAI SDK's standard env vars so the OpenAI
60
+ * clients work from env alone; `baseUrl` carries the gateway's OpenAI-dialect route prefix
61
+ * (`/ai-gateway/openai/v1`). The `NEON_AI_GATEWAY_*` aliases are also emitted: `neonToken`
62
+ * mirrors the OpenAI key, and `neonBaseUrl` is the bare branch gateway host
63
+ * (`scheme://host`, no path) — the `@ai-sdk/neon` provider appends the
64
+ * `/ai-gateway/<dialect>/…` routes itself (https://github.com/vercel/ai/pull/15997).
65
+ */
66
+ aiGateway: {
67
+ apiKey: "OPENAI_API_KEY",
68
+ baseUrl: "OPENAI_BASE_URL",
69
+ neonToken: "NEON_AI_GATEWAY_TOKEN",
70
+ neonBaseUrl: "NEON_AI_GATEWAY_BASE_URL"
71
+ }
72
+ };
73
+ /** OpenAI-dialect route prefix on the branch AI Gateway host. */
74
+ const AI_GATEWAY_OPENAI_PATH = "/ai-gateway/openai/v1";
75
+ /**
76
+ * Resolve the project + branch this process should target, then fetch live Neon
77
+ * connection strings for that branch over the network. Async — calls the Neon API.
78
+ *
79
+ * Use this from build scripts and the `neon-env run` command, where top-level await is
80
+ * fine. For application code that needs a synchronous bootstrap (most frameworks: Drizzle
81
+ * config, Next.js, Vite, etc.), inject env vars via `neon-env run -- <cmd>` and use
82
+ * {@link parseEnv} instead — same {@link NeonEnv} shape, but a sync call against
83
+ * `process.env`.
84
+ *
85
+ * Filesystem- and env-agnostic: pass `projectId` and the target `branchId` explicitly
86
+ * (resolve them in your CLI, e.g. neonctl).
87
+ *
88
+ * ```ts
89
+ * import config from "../neon";
90
+ * import { fetchEnv } from "@neon/env";
91
+ *
92
+ * const env = await fetchEnv(config, { projectId: "patient-art-12345", branchId: "br-…" });
93
+ * const db = drizzle(neon(env.postgres.databaseUrl), { schema });
94
+ * ```
95
+ *
96
+ * The package does **not** mutate `process.env` or the filesystem itself.
97
+ */
98
+ async function fetchEnv(config, options) {
99
+ const api = options.api ?? createApiFromOptions(options);
100
+ const projectId = options.projectId;
101
+ const branches = await api.listBranches(projectId);
102
+ if (branches.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: project ${projectId} has no branches.`, "Deploy your neon.ts policy (or create a branch) first, or pick a different project id."].join(" "), { details: { projectId } });
103
+ const branch = resolveBranch(options.branchId, branches);
104
+ const desired = resolveConfig(config, {
105
+ name: branch.name,
106
+ id: branch.id,
107
+ exists: true,
108
+ ...branch.parentId ? { parentId: branch.parentId } : {},
109
+ isDefault: branch.isDefault,
110
+ isProtected: branch.protected,
111
+ ...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
112
+ });
113
+ const [roles, databases] = await Promise.all([api.listBranchRoles(projectId, branch.id), api.listBranchDatabases(projectId, branch.id)]);
114
+ const roleName = pickRoleName(roles, branch, options.roleName);
115
+ const databaseName = pickDatabaseName(databases, branch, roleName, options.databaseName);
116
+ const wantsAuth = desired.authEnabled;
117
+ const wantsDataApi = desired.dataApiEnabled;
118
+ const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
119
+ api.getConnectionUri(projectId, {
120
+ branchId: branch.id,
121
+ databaseName,
122
+ roleName,
123
+ pooled: true
124
+ }),
125
+ api.getConnectionUri(projectId, {
126
+ branchId: branch.id,
127
+ databaseName,
128
+ roleName,
129
+ pooled: false
130
+ }),
131
+ wantsAuth ? api.getNeonAuth(projectId, branch.id) : Promise.resolve(null),
132
+ wantsDataApi ? api.getNeonDataApi(projectId, branch.id, databaseName) : Promise.resolve(null)
133
+ ]);
134
+ const result = {
135
+ postgres: {
136
+ databaseUrl: pooled.uri,
137
+ databaseUrlUnpooled: unpooled.uri
138
+ },
139
+ branch: { name: branch.name }
140
+ };
141
+ if (wantsAuth) {
142
+ if (!authSnapshot) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or `npx neonctl …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false."].join(" "), { details: {
143
+ projectId,
144
+ branchId: branch.id
145
+ } });
146
+ const envSource = options.env ?? process.env;
147
+ result.auth = {
148
+ baseUrl: resolveAuthBaseUrl(authSnapshot.baseUrl, envSource),
149
+ jwksUrl: resolveAuthJwksUrl(authSnapshot.jwksUrl, envSource)
150
+ };
151
+ }
152
+ if (wantsDataApi) {
153
+ if (!dataApiSnapshot) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${databaseName}.`, "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false."].join(" "), { details: {
154
+ projectId,
155
+ branchId: branch.id,
156
+ databaseName
157
+ } });
158
+ result.dataApi = { url: dataApiSnapshot.url };
159
+ }
160
+ const wantsStorage = (desired.preview?.buckets.length ?? 0) > 0;
161
+ const wantsAiGateway = desired.preview?.aiGatewayEnabled ?? false;
162
+ if (wantsStorage || wantsAiGateway) {
163
+ const secrets = await resolveCredentialSecrets({
164
+ api,
165
+ projectId,
166
+ branchId: branch.id,
167
+ branchName: branch.name,
168
+ scopes: previewCredentialScopes(desired.preview),
169
+ env: options.env ?? process.env,
170
+ needStorage: wantsStorage,
171
+ needApiToken: wantsAiGateway
172
+ });
173
+ if (wantsStorage) {
174
+ const storage = await api.getProjectBranchStorage(projectId, branch.id);
175
+ if (!storage) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy declares object storage (preview.buckets) but storage is not enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or in the Neon Console) — then re-run fetchEnv. Or remove preview.buckets."].join(" "), { details: {
176
+ projectId,
177
+ branchId: branch.id
178
+ } });
179
+ result.storage = {
180
+ accessKeyId: secrets.accessKeyId,
181
+ secretAccessKey: secrets.secretAccessKey,
182
+ endpoint: storage.s3Endpoint,
183
+ region: storage.region
184
+ };
185
+ }
186
+ if (wantsAiGateway) result.aiGateway = {
187
+ apiKey: secrets.apiToken,
188
+ baseUrl: aiGatewayBaseUrl(branch.id, unpooled.uri)
189
+ };
190
+ }
191
+ return result;
192
+ }
193
+ /**
194
+ * Scopes the branch credential should carry for a resolved branch policy. Only object storage
195
+ * and the AI Gateway *require* a credential; functions never force one (they have no credential
196
+ * of their own), but `functions:invoke` is added to the scope set when a credential is already
197
+ * being minted for storage / the AI Gateway, so the one credential can invoke the branch's
198
+ * functions too. Returns `[]` only when nothing credential-bearing is enabled.
199
+ */
200
+ function previewCredentialScopes(preview) {
201
+ if (!preview) return [];
202
+ const storage = preview.buckets.length > 0;
203
+ const aiGateway = preview.aiGatewayEnabled;
204
+ if (!storage && !aiGateway) return [];
205
+ return deriveCredentialScopes({
206
+ storage,
207
+ aiGateway,
208
+ functions: preview.functions.length > 0
209
+ });
210
+ }
211
+ /**
212
+ * Resolve the branch credential's secrets, reusing the ones already in the env source when
213
+ * present and minting a fresh `user` credential otherwise. The Neon API returns `api_token` /
214
+ * `s3_secret_access_key` exactly once at mint time, so the persisted copies (e.g. in
215
+ * `.env.local`, surfaced as `OPENAI_API_KEY` / `AWS_SECRET_ACCESS_KEY`) are the only way to
216
+ * recover them — exactly how one-time Auth keys are round-tripped. Reuse is presence-based
217
+ * (no extra bookkeeping vars): if every secret the enabled features need is already present,
218
+ * reuse it; otherwise mint one credential covering all currently-needed scopes.
219
+ */
220
+ async function resolveCredentialSecrets(args) {
221
+ const sKeys = NEON_ENV_VAR_KEYS.storage;
222
+ const aKeys = NEON_ENV_VAR_KEYS.aiGateway;
223
+ const haveStorage = !args.needStorage || Boolean(args.env[sKeys.accessKeyId] && args.env[sKeys.secretAccessKey]);
224
+ const haveApiToken = !args.needApiToken || Boolean(args.env[aKeys.apiKey]);
225
+ if (haveStorage && haveApiToken) return {
226
+ accessKeyId: args.env[sKeys.accessKeyId] ?? "",
227
+ secretAccessKey: args.env[sKeys.secretAccessKey] ?? "",
228
+ apiToken: args.env[aKeys.apiKey] ?? ""
229
+ };
230
+ const minted = await args.api.createCredential(args.projectId, args.branchId, {
231
+ scopes: args.scopes,
232
+ principalType: "user",
233
+ name: `neon-env ${args.branchName}`
234
+ });
235
+ return {
236
+ accessKeyId: minted.tokenId,
237
+ secretAccessKey: minted.s3SecretAccessKey,
238
+ apiToken: minted.apiToken
239
+ };
240
+ }
241
+ /**
242
+ * The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
243
+ * control-plane API origin. Derive the suffix from the branch's own Postgres connection host
244
+ * by dropping only the endpoint label (the first segment) and keeping everything after it,
245
+ * including any infra cell prefix (`c-N.`): a connection host of
246
+ * `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
247
+ * `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
248
+ * the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
249
+ */
250
+ function aiGatewayHost(branchId, connectionUri) {
251
+ let connectionHost = "";
252
+ try {
253
+ connectionHost = new URL(connectionUri).hostname;
254
+ } catch {
255
+ connectionHost = "";
256
+ }
257
+ return `${branchId}-api.ai.${connectionHost.split(".").slice(1).join(".")}`;
258
+ }
259
+ /** The AI Gateway's OpenAI-dialect base URL (`OPENAI_BASE_URL`) on the branch gateway host. */
260
+ function aiGatewayBaseUrl(branchId, connectionUri) {
261
+ return `https://${aiGatewayHost(branchId, connectionUri)}${AI_GATEWAY_OPENAI_PATH}`;
262
+ }
263
+ /**
264
+ * Resolve the Neon Auth base URL to surface in `env.auth`. Prefer the value returned by
265
+ * the integration (`getNeonAuth` includes it); fall back to whatever is already in the
266
+ * caller's env source so older integrations created before `base_url` was returned still
267
+ * round-trip through `env run`.
268
+ */
269
+ function resolveAuthBaseUrl(snapshotBaseUrl, source) {
270
+ if (snapshotBaseUrl && snapshotBaseUrl !== "") return snapshotBaseUrl;
271
+ return source[NEON_ENV_VAR_KEYS.auth.baseUrl] ?? "";
272
+ }
273
+ /**
274
+ * Resolve the Neon Auth JWKS URL to surface in `env.auth`. Prefer the value returned by the
275
+ * integration (`getNeonAuth` always includes `jwks_url`); fall back to the caller's env
276
+ * source so the value still round-trips through `env run` if a snapshot ever omits it.
277
+ */
278
+ function resolveAuthJwksUrl(snapshotJwksUrl, source) {
279
+ if (snapshotJwksUrl && snapshotJwksUrl !== "") return snapshotJwksUrl;
280
+ return source[NEON_ENV_VAR_KEYS.auth.jwksUrl] ?? "";
281
+ }
282
+ function createApiFromOptions(options) {
283
+ return createNeonApiFromOptions("fetchEnv", {
284
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
285
+ ...options.apiHost ? { apiHost: options.apiHost } : {}
286
+ });
287
+ }
288
+ function resolveBranch(branchId, branches) {
289
+ const match = branches.find((b) => b.id === branchId);
290
+ if (match) return match;
291
+ throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch id ${JSON.stringify(branchId)} not found on project.`, `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`].join(" "), { details: {
292
+ branchId,
293
+ available: branches.map((b) => b.id)
294
+ } });
295
+ }
296
+ function pickRoleName(roles, branch, requested) {
297
+ if (requested) {
298
+ if (!roles.some((r) => r.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`].join(" "), { details: {
299
+ branchId: branch.id,
300
+ roleName: requested,
301
+ availableRoles: roles.map((r) => r.name)
302
+ } });
303
+ return requested;
304
+ }
305
+ if (roles.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`, "Create one via the Neon console or pass `roleName` explicitly."].join(" "), { details: { branchId: branch.id } });
306
+ if (roles.length === 1) return roles[0].name;
307
+ const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
308
+ if (owner) return owner.name;
309
+ const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
310
+ if (appRoles.length === 1) return appRoles[0].name;
311
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles and none is "${NEON_DEFAULT_OWNER_ROLE}"; cannot auto-pick.`, `Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`].join(" "), { details: {
312
+ branchId: branch.id,
313
+ availableRoles: roles.map((r) => r.name)
314
+ } });
315
+ }
316
+ function pickDatabaseName(databases, branch, roleName, requested) {
317
+ if (requested) {
318
+ if (!databases.some((d) => d.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`].join(" "), { details: {
319
+ branchId: branch.id,
320
+ databaseName: requested,
321
+ availableDatabases: databases.map((d) => d.name)
322
+ } });
323
+ return requested;
324
+ }
325
+ if (databases.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`, "Create one via the Neon console or pass `databaseName` explicitly."].join(" "), { details: { branchId: branch.id } });
326
+ if (databases.length === 1) return databases[0].name;
327
+ const owned = databases.filter((d) => d.ownerName === roleName);
328
+ if (owned.length === 1) return owned[0].name;
329
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases; cannot auto-pick.`, `Pass \`databaseName\` explicitly. Available: ${databases.map((d) => d.name).join(", ")}.`].join(" "), { details: {
330
+ branchId: branch.id,
331
+ availableDatabases: databases.map((d) => d.name)
332
+ } });
333
+ }
334
+ /**
335
+ * Per-namespace zod schemas. Each defines exactly the OS-level keys parsed from
336
+ * `process.env` for its namespace. Keep in sync with {@link NEON_ENV_VAR_KEYS}.
337
+ *
338
+ * `z.string().url()` would be tighter than `min(1)` but Postgres URIs that include
339
+ * URL-illegal characters in the password (rare but legal in Neon's connection-string
340
+ * format) fail the WHATWG `URL` parse, so we settle for "non-empty string".
341
+ */
342
+ const postgresEnvSchema = z.object({
343
+ DATABASE_URL: z.string({ message: "DATABASE_URL is missing" }).min(1, "DATABASE_URL must not be empty"),
344
+ DATABASE_URL_UNPOOLED: z.string({ message: "DATABASE_URL_UNPOOLED is missing" }).min(1, "DATABASE_URL_UNPOOLED must not be empty")
345
+ });
346
+ const authEnvSchema = z.object({
347
+ NEON_AUTH_BASE_URL: z.string({ message: "NEON_AUTH_BASE_URL is missing" }).min(1, "NEON_AUTH_BASE_URL must not be empty"),
348
+ NEON_AUTH_JWKS_URL: z.string({ message: "NEON_AUTH_JWKS_URL is missing" }).min(1, "NEON_AUTH_JWKS_URL must not be empty")
349
+ });
350
+ const dataApiEnvSchema = z.object({ NEON_DATA_API_URL: z.string({ message: "NEON_DATA_API_URL is missing" }).min(1, "NEON_DATA_API_URL must not be empty") });
351
+ const storageEnvSchema = z.object({
352
+ AWS_ACCESS_KEY_ID: z.string({ message: "AWS_ACCESS_KEY_ID is missing" }).min(1, "AWS_ACCESS_KEY_ID must not be empty"),
353
+ AWS_SECRET_ACCESS_KEY: z.string({ message: "AWS_SECRET_ACCESS_KEY is missing" }).min(1, "AWS_SECRET_ACCESS_KEY must not be empty"),
354
+ AWS_ENDPOINT_URL_S3: z.string({ message: "AWS_ENDPOINT_URL_S3 is missing" }).min(1, "AWS_ENDPOINT_URL_S3 must not be empty"),
355
+ AWS_REGION: z.string({ message: "AWS_REGION is missing" }).min(1, "AWS_REGION must not be empty")
356
+ });
357
+ const aiGatewayEnvSchema = z.object({
358
+ OPENAI_API_KEY: z.string({ message: "OPENAI_API_KEY is missing" }).min(1, "OPENAI_API_KEY must not be empty"),
359
+ OPENAI_BASE_URL: z.string({ message: "OPENAI_BASE_URL is missing" }).min(1, "OPENAI_BASE_URL must not be empty")
360
+ });
361
+ /** Whether a **static** policy declares object storage (`preview.buckets`). No network. */
362
+ function configWantsStorage(config) {
363
+ return Object.keys(config.preview?.buckets ?? {}).length > 0;
364
+ }
365
+ /** Whether a **static** policy enables the AI Gateway (`preview.aiGateway`). No network. */
366
+ function configWantsAiGateway(config) {
367
+ return isServiceEnabledInput(config.preview?.aiGateway);
368
+ }
369
+ /** Static-toggle helper mirroring `config`'s `isServiceEnabled` for the env reader. */
370
+ function isServiceEnabledInput(toggle) {
371
+ if (toggle === void 0) return false;
372
+ if (typeof toggle === "boolean") return toggle;
373
+ return toggle.enabled !== false;
374
+ }
375
+ function parseEnv(config, scopeOrKeys) {
376
+ const source = process.env;
377
+ if (Array.isArray(scopeOrKeys)) return parseFilteredEnv(source, scopeOrKeys);
378
+ const scope = typeof scopeOrKeys === "string" ? scopeOrKeys : void 0;
379
+ const issues = [];
380
+ const result = {};
381
+ const pg = postgresEnvSchema.safeParse({
382
+ DATABASE_URL: source.DATABASE_URL,
383
+ DATABASE_URL_UNPOOLED: source.DATABASE_URL_UNPOOLED
384
+ });
385
+ if (pg.success) result.postgres = {
386
+ databaseUrl: pg.data.DATABASE_URL,
387
+ databaseUrlUnpooled: pg.data.DATABASE_URL_UNPOOLED
388
+ };
389
+ else for (const issue of pg.error.issues) issues.push(issue.message);
390
+ const branchName = source[NEON_ENV_VAR_KEYS.branch.name];
391
+ if (branchName !== void 0 && branchName !== "") result.branch = { name: branchName };
392
+ if (isServiceEnabledInput(config.auth)) {
393
+ const auth = authEnvSchema.safeParse({
394
+ NEON_AUTH_BASE_URL: source.NEON_AUTH_BASE_URL,
395
+ NEON_AUTH_JWKS_URL: source.NEON_AUTH_JWKS_URL
396
+ });
397
+ if (auth.success) result.auth = {
398
+ baseUrl: auth.data.NEON_AUTH_BASE_URL,
399
+ jwksUrl: auth.data.NEON_AUTH_JWKS_URL
400
+ };
401
+ else for (const issue of auth.error.issues) issues.push(issue.message);
402
+ }
403
+ if (isServiceEnabledInput(config.dataApi)) {
404
+ const dataApi = dataApiEnvSchema.safeParse({ NEON_DATA_API_URL: source.NEON_DATA_API_URL });
405
+ if (dataApi.success) result.dataApi = { url: dataApi.data.NEON_DATA_API_URL };
406
+ else for (const issue of dataApi.error.issues) issues.push(issue.message);
407
+ }
408
+ if (configWantsStorage(config)) {
409
+ const storage = storageEnvSchema.safeParse({
410
+ AWS_ACCESS_KEY_ID: source.AWS_ACCESS_KEY_ID,
411
+ AWS_SECRET_ACCESS_KEY: source.AWS_SECRET_ACCESS_KEY,
412
+ AWS_ENDPOINT_URL_S3: source.AWS_ENDPOINT_URL_S3,
413
+ AWS_REGION: source.AWS_REGION
414
+ });
415
+ if (storage.success) result.storage = {
416
+ accessKeyId: storage.data.AWS_ACCESS_KEY_ID,
417
+ secretAccessKey: storage.data.AWS_SECRET_ACCESS_KEY,
418
+ endpoint: storage.data.AWS_ENDPOINT_URL_S3,
419
+ region: storage.data.AWS_REGION
420
+ };
421
+ else for (const issue of storage.error.issues) issues.push(issue.message);
422
+ }
423
+ if (configWantsAiGateway(config)) {
424
+ const aiGateway = aiGatewayEnvSchema.safeParse({
425
+ OPENAI_API_KEY: source.OPENAI_API_KEY,
426
+ OPENAI_BASE_URL: source.OPENAI_BASE_URL
427
+ });
428
+ if (aiGateway.success) result.aiGateway = {
429
+ apiKey: aiGateway.data.OPENAI_API_KEY,
430
+ baseUrl: aiGateway.data.OPENAI_BASE_URL
431
+ };
432
+ else for (const issue of aiGateway.error.issues) issues.push(issue.message);
433
+ }
434
+ if (scope !== void 0) {
435
+ const fn = config.preview?.functions?.[scope];
436
+ if (!fn) throw new PlatformError(ErrorCode.EnvNotInjected, [`parseEnv: no function "${scope}" is declared in this policy's preview.functions.`, "Pass a declared function slug (or omit the scope to read external env)."].join("\n"), { details: { scope } });
437
+ const envOut = {};
438
+ for (const key of Object.keys(fn.env ?? {})) {
439
+ const value = source[key];
440
+ if (value === void 0) issues.push(`${key} is missing (function "${scope}")`);
441
+ else envOut[key] = value;
442
+ }
443
+ result.function = envOut;
444
+ }
445
+ if (issues.length > 0) throw new PlatformError(ErrorCode.EnvNotInjected, [
446
+ "parseEnv: the required Neon env variables are not present in process.env.",
447
+ ...issues.map((i) => ` - ${i}`),
448
+ "Inject them via one of:",
449
+ " - `neon dev` / `neon-env run -- <your dev command>` (wraps the command with the vars injected)",
450
+ " - your hosting platform's Neon integration (Vercel, Fly, Railway, …)",
451
+ " - for the `function` namespace: deploy the function (`neon deploy` / `config apply`) so its env is uploaded.",
452
+ "Or switch the call to `await fetchEnv(config, …)` if you're in a context that can do async I/O."
453
+ ].join("\n"), { details: { missing: issues } });
454
+ return result;
455
+ }
456
+ /**
457
+ * Runtime reverse map for filtered `parseEnv`: OS-level env-var key → `[namespace, property]`
458
+ * in the {@link NeonEnv} shape. The compile-time mirror is {@link EnvKeysByNamespace} /
459
+ * {@link EnvKeyToProp}; keep all three in sync. Only input vars appear (no output-only
460
+ * aliases).
461
+ */
462
+ const FILTERABLE_ENV_KEYS = {
463
+ DATABASE_URL: ["postgres", "databaseUrl"],
464
+ DATABASE_URL_UNPOOLED: ["postgres", "databaseUrlUnpooled"],
465
+ NEON_AUTH_BASE_URL: ["auth", "baseUrl"],
466
+ NEON_AUTH_JWKS_URL: ["auth", "jwksUrl"],
467
+ NEON_DATA_API_URL: ["dataApi", "url"],
468
+ AWS_ACCESS_KEY_ID: ["storage", "accessKeyId"],
469
+ AWS_SECRET_ACCESS_KEY: ["storage", "secretAccessKey"],
470
+ AWS_ENDPOINT_URL_S3: ["storage", "endpoint"],
471
+ AWS_REGION: ["storage", "region"],
472
+ OPENAI_API_KEY: ["aiGateway", "apiKey"],
473
+ OPENAI_BASE_URL: ["aiGateway", "baseUrl"]
474
+ };
475
+ /**
476
+ * Filtered counterpart to the {@link parseEnv} body: validate and return only the explicitly
477
+ * selected OS-level env-var keys, projected back into the narrowed namespaced shape. Unlike
478
+ * the full reader it never consults the policy — the selection alone decides what's required —
479
+ * so vars the caller didn't ask for (e.g. `DATABASE_URL_UNPOOLED`) can be absent without
480
+ * throwing. Mirrors the same non-empty constraint and {@link PlatformError} aggregation.
481
+ */
482
+ function parseFilteredEnv(source, keys) {
483
+ const issues = [];
484
+ const result = {};
485
+ for (const key of keys) {
486
+ if (!Object.hasOwn(FILTERABLE_ENV_KEYS, key)) {
487
+ issues.push(`${key} is not a selectable Neon env variable`);
488
+ continue;
489
+ }
490
+ const value = source[key];
491
+ if (value === void 0) {
492
+ issues.push(`${key} is missing`);
493
+ continue;
494
+ }
495
+ if (value === "") {
496
+ issues.push(`${key} must not be empty`);
497
+ continue;
498
+ }
499
+ const [namespace, property] = FILTERABLE_ENV_KEYS[key];
500
+ const bucket = result[namespace] ?? {};
501
+ bucket[property] = value;
502
+ result[namespace] = bucket;
503
+ }
504
+ if (issues.length > 0) throw new PlatformError(ErrorCode.EnvNotInjected, [
505
+ "parseEnv: the required Neon env variables are not present in process.env.",
506
+ ...issues.map((i) => ` - ${i}`),
507
+ "Inject them via one of:",
508
+ " - `neon dev` / `neon-env run -- <your dev command>` (wraps the command with the vars injected)",
509
+ " - your hosting platform's Neon integration (Vercel, Fly, Railway, …)",
510
+ "Or switch the call to `await fetchEnv(config, …)` if you're in a context that can do async I/O."
511
+ ].join("\n"), { details: { missing: issues } });
512
+ return result;
513
+ }
514
+ /**
515
+ * Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
516
+ * for cross-process transport. Named after the web-platform `.entries()` convention
517
+ * (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
518
+ * iterator of tuples since that's the shape env injection needs (wrap with
519
+ * `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
520
+ * to inject the vars into a subprocess's `process.env`.
521
+ *
522
+ * Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
523
+ * conditional namespaces are present.
524
+ */
525
+ function toEntries(env) {
526
+ const out = {
527
+ [NEON_ENV_VAR_KEYS.postgres.databaseUrl]: env.postgres.databaseUrl,
528
+ [NEON_ENV_VAR_KEYS.postgres.databaseUrlUnpooled]: env.postgres.databaseUrlUnpooled
529
+ };
530
+ if (env.branch) out[NEON_ENV_VAR_KEYS.branch.name] = env.branch.name;
531
+ const withAuth = env;
532
+ if (withAuth.auth) {
533
+ out[NEON_ENV_VAR_KEYS.auth.baseUrl] = withAuth.auth.baseUrl;
534
+ out[NEON_ENV_VAR_KEYS.auth.jwksUrl] = withAuth.auth.jwksUrl;
535
+ }
536
+ const withDataApi = env;
537
+ if (withDataApi.dataApi) out[NEON_ENV_VAR_KEYS.dataApi.url] = withDataApi.dataApi.url;
538
+ const withStorage = env;
539
+ if (withStorage.storage) {
540
+ const s = withStorage.storage;
541
+ const keys = NEON_ENV_VAR_KEYS.storage;
542
+ out[keys.accessKeyId] = s.accessKeyId;
543
+ out[keys.secretAccessKey] = s.secretAccessKey;
544
+ out[keys.endpoint] = s.endpoint;
545
+ out[keys.region] = s.region;
546
+ }
547
+ const withAiGateway = env;
548
+ if (withAiGateway.aiGateway) {
549
+ const keys = NEON_ENV_VAR_KEYS.aiGateway;
550
+ const ai = withAiGateway.aiGateway;
551
+ out[keys.apiKey] = ai.apiKey;
552
+ out[keys.baseUrl] = ai.baseUrl;
553
+ out[keys.neonToken] = ai.apiKey;
554
+ out[keys.neonBaseUrl] = new URL(ai.baseUrl).origin;
555
+ }
556
+ return out;
557
+ }
558
+ //#endregion
559
+ export { NEON_ENV_VAR_KEYS, fetchEnv, parseEnv, toEntries };
560
+
561
+ //# sourceMappingURL=env.js.map