@neondatabase/env 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE.md +178 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +61 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config/dist/lib/neon-api.d.ts +264 -0
  6. package/dist/config/dist/lib/neon-api.d.ts.map +1 -0
  7. package/dist/config/dist/lib/types.d.ts +184 -0
  8. package/dist/config/dist/lib/types.d.ts.map +1 -0
  9. package/dist/config/dist/v1.d.ts +4 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +3 -0
  12. package/dist/lib/cli/commands.d.ts +46 -0
  13. package/dist/lib/cli/commands.d.ts.map +1 -0
  14. package/dist/lib/cli/commands.js +181 -0
  15. package/dist/lib/cli/commands.js.map +1 -0
  16. package/dist/lib/cli/resolve-context.d.ts +35 -0
  17. package/dist/lib/cli/resolve-context.d.ts.map +1 -0
  18. package/dist/lib/cli/resolve-context.js +90 -0
  19. package/dist/lib/cli/resolve-context.js.map +1 -0
  20. package/dist/lib/env.d.ts +194 -0
  21. package/dist/lib/env.d.ts.map +1 -0
  22. package/dist/lib/env.js +263 -0
  23. package/dist/lib/env.js.map +1 -0
  24. package/dist/v1.d.ts +2 -0
  25. package/dist/v1.js +2 -0
  26. package/package.json +72 -21
  27. package/.env.example +0 -5
  28. package/e2e/env.e2e.test.ts +0 -36
  29. package/e2e/helpers.ts +0 -188
  30. package/e2e/load-env.ts +0 -29
  31. package/e2e/setup.ts +0 -24
  32. package/src/cli.ts +0 -107
  33. package/src/index.ts +0 -5
  34. package/src/lib/cli/commands.test.ts +0 -101
  35. package/src/lib/cli/commands.ts +0 -267
  36. package/src/lib/cli/resolve-context.test.ts +0 -242
  37. package/src/lib/cli/resolve-context.ts +0 -142
  38. package/src/lib/env.test.ts +0 -172
  39. package/src/lib/env.ts +0 -610
  40. package/src/lib/fake-neon-api.ts +0 -782
  41. package/src/lib/test-utils.ts +0 -83
  42. package/src/v1.ts +0 -32
  43. package/tsconfig.json +0 -4
  44. package/tsdown.config.ts +0 -20
  45. package/vitest.config.ts +0 -19
  46. package/vitest.e2e.config.ts +0 -29
package/src/lib/env.ts DELETED
@@ -1,610 +0,0 @@
1
- import {
2
- type BranchConfig,
3
- type Config,
4
- createNeonApiFromOptions,
5
- ErrorCode,
6
- type NeonApi,
7
- type NeonBranchSnapshot,
8
- type NeonDatabaseSnapshot,
9
- type NeonRoleSnapshot,
10
- PlatformError,
11
- resolveConfig,
12
- } from "@neondatabase/config/v1";
13
- import { z } from "zod";
14
-
15
- /**
16
- * Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
17
- * for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
18
- * that talks to `process.env`).
19
- *
20
- * Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
21
- * camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
22
- * by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
23
- * {@link dataApiEnvSchema}.
24
- */
25
- export const NEON_ENV_VAR_KEYS = {
26
- postgres: {
27
- databaseUrl: "DATABASE_URL",
28
- databaseUrlUnpooled: "DATABASE_URL_UNPOOLED",
29
- },
30
- auth: {
31
- baseUrl: "NEON_AUTH_BASE_URL",
32
- },
33
- dataApi: {
34
- url: "NEON_DATA_API_URL",
35
- },
36
- } as const;
37
-
38
- /** Per-namespace inner shapes. Exposed so consumers can name the parts independently. */
39
- export interface NeonPostgresEnv {
40
- /**
41
- * Pooled connection string (via Neon's PgBouncer pooler). The right default for
42
- * serverless drivers (`@neondatabase/serverless`, edge runtimes, Postgres.js, …).
43
- */
44
- databaseUrl: string;
45
- /**
46
- * Direct (unpooled) connection string. Use this when you need session-level
47
- * features (`LISTEN`/`NOTIFY`, prepared statements across calls, transactions
48
- * spanning round-trips) that PgBouncer's transaction-mode pooling drops.
49
- */
50
- databaseUrlUnpooled: string;
51
- }
52
-
53
- /**
54
- * Bits of a Neon Auth integration for the resolved branch. Only present on `NeonEnv`
55
- * when the branch policy enables `auth`.
56
- *
57
- * Neon Auth exposes a single `baseUrl` that doubles as the publishable client identifier
58
- * — the rest of the surface (project id, JWKS URL, …) is derived from it at runtime by
59
- * the Neon Auth SDK. `fetchEnv` reads it from the live integration; `parseEnv` reads it
60
- * from `process.env` (`NEON_AUTH_BASE_URL`).
61
- */
62
- export interface NeonAuthEnv {
63
- baseUrl: string;
64
- }
65
-
66
- /** Bits of a Neon Data API integration. Only present when the branch policy enables it. */
67
- export interface NeonDataApiEnv {
68
- url: string;
69
- }
70
-
71
- /**
72
- * Empty record alias used as the "false" branch of the conditional namespace adds below.
73
- * `Record<never, never>` is the no-op for intersection — the cleaner alternative to `{}`,
74
- * which biome rejects (it means "any non-null", not "empty object").
75
- */
76
- type NoNamespace = Record<never, never>;
77
-
78
- type BranchConfigOf<C extends Config> = ReturnType<C> extends BranchConfig
79
- ? ReturnType<C>
80
- : BranchConfig;
81
-
82
- type ServiceToggleOf<Cfg, Key extends "auth" | "dataApi"> = Cfg extends unknown
83
- ? Key extends keyof Cfg
84
- ? Cfg[Key]
85
- : never
86
- : never;
87
-
88
- type HasEnabledService<Cfg, Key extends "auth" | "dataApi"> = [
89
- Exclude<ServiceToggleOf<Cfg, Key>, undefined | { enabled: false }>,
90
- ] extends [never]
91
- ? false
92
- : true;
93
-
94
- type IsDefaultConfig<C extends Config> = Config extends C ? true : false;
95
-
96
- /**
97
- * Static, namespaced shape of `fetchEnv` / `parseEnv`'s return value. Generic over the
98
- * {@link Config} so the type system knows which optional namespaces are present.
99
- *
100
- * - `postgres` is always present.
101
- * - `auth` is added iff the config return type has an `auth` namespace that is not
102
- * explicitly disabled.
103
- * - `dataApi` is added iff the config return type has a `dataApi` namespace that is not
104
- * explicitly disabled.
105
- */
106
- export type NeonEnv<C extends Config = Config> = {
107
- postgres: NeonPostgresEnv;
108
- } & (IsDefaultConfig<C> extends true
109
- ? NoNamespace
110
- : HasEnabledService<BranchConfigOf<C>, "auth"> extends true
111
- ? { auth: NeonAuthEnv }
112
- : NoNamespace) &
113
- (IsDefaultConfig<C> extends true
114
- ? NoNamespace
115
- : HasEnabledService<BranchConfigOf<C>, "dataApi"> extends true
116
- ? { dataApi: NeonDataApiEnv }
117
- : NoNamespace);
118
-
119
- export interface FetchEnvOptions {
120
- /**
121
- * Neon project id. **Required** — the management API addresses branches through their
122
- * project. Resolve it in your CLI (e.g. neonctl) and pass it in.
123
- */
124
- projectId: string;
125
- /** Neon branch id (`br-…`). **Required.** Resolve names to ids before calling. */
126
- branchId: string;
127
- /**
128
- * Neon API key. Resolved via the standard chain (option → `NEON_API_KEY` →
129
- * `~/.config/neonctl/credentials.json`) when omitted. Ignored when a custom `api`
130
- * is supplied.
131
- */
132
- apiKey?: string;
133
- /**
134
- * Inject a custom NeonApi adapter. Primarily used by tests; production callers can rely
135
- * on the default real adapter built from `apiKey`.
136
- */
137
- api?: NeonApi;
138
- /**
139
- * Role name to fetch credentials for. When omitted, the only role on the branch is
140
- * auto-picked; throws {@link PlatformError} with `PLATFORM_AMBIGUOUS_BRANCH_AUTH` if
141
- * the branch has more than one role.
142
- */
143
- roleName?: string;
144
- /**
145
- * Database name. When omitted, the only database on the branch is auto-picked; throws
146
- * {@link PlatformError} with `PLATFORM_AMBIGUOUS_BRANCH_AUTH` if the branch has more
147
- * than one database.
148
- */
149
- databaseName?: string;
150
- /**
151
- * Env source used for one-time Auth keys that cannot be refetched after integration
152
- * creation. Defaults to `process.env`; callers may layer values from `.env.local`.
153
- */
154
- env?: NodeJS.ProcessEnv;
155
- }
156
-
157
- /**
158
- * Resolve the project + branch this process should target, then fetch live Neon
159
- * connection strings for that branch over the network. Async — calls the Neon API.
160
- *
161
- * Use this from build scripts and the `neon-env run` command, where top-level await is
162
- * fine. For application code that needs a synchronous bootstrap (most frameworks: Drizzle
163
- * config, Next.js, Vite, etc.), inject env vars via `neon-env run -- <cmd>` and use
164
- * {@link parseEnv} instead — same {@link NeonEnv} shape, but a sync call against
165
- * `process.env`.
166
- *
167
- * Filesystem- and env-agnostic: pass `projectId` and the target `branchId` explicitly
168
- * (resolve them in your CLI, e.g. neonctl).
169
- *
170
- * ```ts
171
- * import config from "../neon";
172
- * import { fetchEnv } from "@neondatabase/env/v1";
173
- *
174
- * const env = await fetchEnv(config, { projectId: "patient-art-12345", branchId: "br-…" });
175
- * const db = drizzle(neon(env.postgres.databaseUrl), { schema });
176
- * ```
177
- *
178
- * The package does **not** mutate `process.env` or the filesystem itself.
179
- */
180
- export async function fetchEnv<const C extends Config>(
181
- config: C,
182
- options: FetchEnvOptions,
183
- ): Promise<NeonEnv<C>> {
184
- const api = options.api ?? createApiFromOptions(options);
185
- const projectId = options.projectId;
186
-
187
- const branches = await api.listBranches(projectId);
188
- if (branches.length === 0) {
189
- throw new PlatformError(
190
- ErrorCode.BranchNotFound,
191
- [
192
- `fetchEnv: project ${projectId} has no branches.`,
193
- "Deploy your neon.ts policy (or create a branch) first, or pick a different project id.",
194
- ].join(" "),
195
- { details: { projectId } },
196
- );
197
- }
198
-
199
- const branch = resolveBranch(options.branchId, branches);
200
- const desired = resolveConfig(config, {
201
- name: branch.name,
202
- id: branch.id,
203
- exists: true,
204
- ...(branch.parentId ? { parentId: branch.parentId } : {}),
205
- isDefault: branch.isDefault,
206
- isProtected: branch.protected,
207
- ...(branch.expiresAt ? { expiresAt: branch.expiresAt } : {}),
208
- });
209
-
210
- const [roles, databases] = await Promise.all([
211
- api.listBranchRoles(projectId, branch.id),
212
- api.listBranchDatabases(projectId, branch.id),
213
- ]);
214
-
215
- const roleName = pickRoleName(roles, branch, options.roleName);
216
- const databaseName = pickDatabaseName(
217
- databases,
218
- branch,
219
- roleName,
220
- options.databaseName,
221
- );
222
-
223
- // Fan out: always fetch both Postgres URIs. Conditionally fetch auth + dataApi based
224
- // on the branch policy. Auth key fields are only returned at integration creation time;
225
- // for Better Auth they may legitimately be empty, so absence in the local env becomes
226
- // empty string values while still emitting the required variable names.
227
- const wantsAuth = desired.authEnabled;
228
- const wantsDataApi = desired.dataApiEnabled;
229
-
230
- const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all(
231
- [
232
- api.getConnectionUri(projectId, {
233
- branchId: branch.id,
234
- databaseName,
235
- roleName,
236
- pooled: true,
237
- }),
238
- api.getConnectionUri(projectId, {
239
- branchId: branch.id,
240
- databaseName,
241
- roleName,
242
- pooled: false,
243
- }),
244
- wantsAuth
245
- ? api.getNeonAuth(projectId, branch.id)
246
- : Promise.resolve(null),
247
- wantsDataApi
248
- ? api.getNeonDataApi(projectId, branch.id, databaseName)
249
- : Promise.resolve(null),
250
- ],
251
- );
252
-
253
- const result: Record<string, unknown> = {
254
- postgres: {
255
- databaseUrl: pooled.uri,
256
- databaseUrlUnpooled: unpooled.uri,
257
- },
258
- };
259
-
260
- if (wantsAuth) {
261
- if (!authSnapshot) {
262
- throw new PlatformError(
263
- ErrorCode.NotFound,
264
- [
265
- `fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`,
266
- "Enable it via `apply(config, { projectId, branchId })` (or `npx neonctl …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false.",
267
- ].join(" "),
268
- {
269
- details: { projectId, branchId: branch.id },
270
- },
271
- );
272
- }
273
- const baseUrl = resolveAuthBaseUrl(
274
- authSnapshot.baseUrl,
275
- options.env ?? process.env,
276
- );
277
- result.auth = { baseUrl } satisfies NeonAuthEnv;
278
- }
279
-
280
- if (wantsDataApi) {
281
- if (!dataApiSnapshot) {
282
- throw new PlatformError(
283
- ErrorCode.NotFound,
284
- [
285
- `fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${databaseName}.`,
286
- "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false.",
287
- ].join(" "),
288
- {
289
- details: {
290
- projectId,
291
- branchId: branch.id,
292
- databaseName,
293
- },
294
- },
295
- );
296
- }
297
- result.dataApi = { url: dataApiSnapshot.url } satisfies NeonDataApiEnv;
298
- }
299
-
300
- return result as NeonEnv<C>;
301
- }
302
-
303
- /**
304
- * Resolve the Neon Auth base URL to surface in `env.auth`. Prefer the value returned by
305
- * the integration (`getNeonAuth` includes it); fall back to whatever is already in the
306
- * caller's env source so older integrations created before `base_url` was returned still
307
- * round-trip through `env run`.
308
- */
309
- function resolveAuthBaseUrl(
310
- snapshotBaseUrl: string | undefined,
311
- source: NodeJS.ProcessEnv,
312
- ): string {
313
- if (snapshotBaseUrl && snapshotBaseUrl !== "") return snapshotBaseUrl;
314
- return source[NEON_ENV_VAR_KEYS.auth.baseUrl] ?? "";
315
- }
316
-
317
- function createApiFromOptions(options: FetchEnvOptions): NeonApi {
318
- return createNeonApiFromOptions(
319
- "fetchEnv",
320
- options.apiKey ? { apiKey: options.apiKey } : {},
321
- );
322
- }
323
-
324
- function resolveBranch(
325
- branchId: string,
326
- branches: NeonBranchSnapshot[],
327
- ): NeonBranchSnapshot {
328
- const match = branches.find((b) => b.id === branchId);
329
- if (match) return match;
330
- throw new PlatformError(
331
- ErrorCode.BranchNotFound,
332
- [
333
- `fetchEnv: branch id ${JSON.stringify(branchId)} not found on project.`,
334
- `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`,
335
- ].join(" "),
336
- {
337
- details: {
338
- branchId,
339
- available: branches.map((b) => b.id),
340
- },
341
- },
342
- );
343
- }
344
-
345
- function pickRoleName(
346
- roles: NeonRoleSnapshot[],
347
- branch: NeonBranchSnapshot,
348
- requested: string | undefined,
349
- ): string {
350
- if (requested) {
351
- if (!roles.some((r) => r.name === requested)) {
352
- throw new PlatformError(
353
- ErrorCode.BranchNotFound,
354
- [
355
- `fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`,
356
- `Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`,
357
- ].join(" "),
358
- {
359
- details: {
360
- branchId: branch.id,
361
- roleName: requested,
362
- availableRoles: roles.map((r) => r.name),
363
- },
364
- },
365
- );
366
- }
367
- return requested;
368
- }
369
- if (roles.length === 0) {
370
- throw new PlatformError(
371
- ErrorCode.BranchNotFound,
372
- [
373
- `fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`,
374
- "Create one via the Neon console or pass `roleName` explicitly.",
375
- ].join(" "),
376
- { details: { branchId: branch.id } },
377
- );
378
- }
379
- if (roles.length === 1) return roles[0].name;
380
- throw new PlatformError(
381
- ErrorCode.AmbiguousBranchAuth,
382
- [
383
- `fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles; cannot auto-pick.`,
384
- `Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`,
385
- ].join(" "),
386
- {
387
- details: {
388
- branchId: branch.id,
389
- availableRoles: roles.map((r) => r.name),
390
- },
391
- },
392
- );
393
- }
394
-
395
- function pickDatabaseName(
396
- databases: NeonDatabaseSnapshot[],
397
- branch: NeonBranchSnapshot,
398
- roleName: string,
399
- requested: string | undefined,
400
- ): string {
401
- if (requested) {
402
- if (!databases.some((d) => d.name === requested)) {
403
- throw new PlatformError(
404
- ErrorCode.BranchNotFound,
405
- [
406
- `fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`,
407
- `Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`,
408
- ].join(" "),
409
- {
410
- details: {
411
- branchId: branch.id,
412
- databaseName: requested,
413
- availableDatabases: databases.map((d) => d.name),
414
- },
415
- },
416
- );
417
- }
418
- return requested;
419
- }
420
- if (databases.length === 0) {
421
- throw new PlatformError(
422
- ErrorCode.BranchNotFound,
423
- [
424
- `fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`,
425
- "Create one via the Neon console or pass `databaseName` explicitly.",
426
- ].join(" "),
427
- { details: { branchId: branch.id } },
428
- );
429
- }
430
- if (databases.length === 1) return databases[0].name;
431
-
432
- // Prefer a database owned by the role we're connecting as.
433
- const owned = databases.filter((d) => d.ownerName === roleName);
434
- if (owned.length === 1) return owned[0].name;
435
-
436
- throw new PlatformError(
437
- ErrorCode.AmbiguousBranchAuth,
438
- [
439
- `fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases; cannot auto-pick.`,
440
- `Pass \`databaseName\` explicitly. Available: ${databases.map((d) => d.name).join(", ")}.`,
441
- ].join(" "),
442
- {
443
- details: {
444
- branchId: branch.id,
445
- availableDatabases: databases.map((d) => d.name),
446
- },
447
- },
448
- );
449
- }
450
-
451
- // ───────────────────────── parseEnv ─────────────────────────
452
-
453
- /**
454
- * Per-namespace zod schemas. Each defines exactly the OS-level keys parsed from
455
- * `process.env` for its namespace. Keep in sync with {@link NEON_ENV_VAR_KEYS}.
456
- *
457
- * `z.string().url()` would be tighter than `min(1)` but Postgres URIs that include
458
- * URL-illegal characters in the password (rare but legal in Neon's connection-string
459
- * format) fail the WHATWG `URL` parse, so we settle for "non-empty string".
460
- */
461
- const postgresEnvSchema = z.object({
462
- DATABASE_URL: z
463
- .string({ message: "DATABASE_URL is missing" })
464
- .min(1, "DATABASE_URL must not be empty"),
465
- DATABASE_URL_UNPOOLED: z
466
- .string({ message: "DATABASE_URL_UNPOOLED is missing" })
467
- .min(1, "DATABASE_URL_UNPOOLED must not be empty"),
468
- });
469
-
470
- const authEnvSchema = z.object({
471
- NEON_AUTH_BASE_URL: z
472
- .string({ message: "NEON_AUTH_BASE_URL is missing" })
473
- .min(1, "NEON_AUTH_BASE_URL must not be empty"),
474
- });
475
-
476
- const dataApiEnvSchema = z.object({
477
- NEON_DATA_API_URL: z
478
- .string({ message: "NEON_DATA_API_URL is missing" })
479
- .min(1, "NEON_DATA_API_URL must not be empty"),
480
- });
481
-
482
- /**
483
- * Synchronous, network-free counterpart to {@link fetchEnv}. Reads `process.env` (or
484
- * `options.env`), validates the required Neon env vars with zod, and returns the same
485
- * {@link NeonEnv} shape — so the rest of your app touches `env.postgres.databaseUrl`
486
- * instead of stringly-typed `process.env.DATABASE_URL` lookups.
487
- *
488
- * Designed for the **"env-vars-already-injected"** path:
489
- * - You wrapped your dev command with `neon-env run -- <cmd>`.
490
- * - Your platform (Vercel, Fly, Railway, …) injected the vars via its own integration.
491
- *
492
- * Takes a branch **name** (not an id like the API-backed `fetchEnv` / config operations):
493
- * `parseEnv` makes no Neon API call, so the only thing it needs the branch for is
494
- * evaluating your `neon.ts` policy, which switches on `branch.name`. With no network round
495
- * trip there's no way to turn a `br-…` id into a name, so the name is passed directly.
496
- * Pass it explicitly so the result is deterministic and not coupled to any `NEON_*` env
497
- * var. (The `neon-env` CLI injects `NEON_BRANCH_NAME`; pass that through, or default to
498
- * your main branch name.) Prefer `fetchEnv` when runtime code needs the exact live branch.
499
- *
500
- * Throws `PlatformError(EnvNotInjected)` listing every missing/invalid var when the env
501
- * isn't fully populated, with a fix hint pointing back at `neon-env run`.
502
- *
503
- * ```ts
504
- * import config from "../neon";
505
- * import { parseEnv } from "@neondatabase/env/v1";
506
- *
507
- * const env = parseEnv(config, process.env.NEON_BRANCH_NAME ?? "main");
508
- * const db = drizzle(neon(env.postgres.databaseUrl), { schema });
509
- * // env.auth is statically typed when the config return type has auth: {} or auth.enabled: true.
510
- * ```
511
- */
512
- export function parseEnv<const C extends Config>(
513
- config: C,
514
- branchName: string,
515
- ): NeonEnv<C> {
516
- const source = process.env;
517
- const issues: string[] = [];
518
- const result: Record<string, unknown> = {};
519
- const desired = resolveConfig(config, {
520
- name: branchName,
521
- exists: true,
522
- });
523
-
524
- const pg = postgresEnvSchema.safeParse({
525
- DATABASE_URL: source.DATABASE_URL,
526
- DATABASE_URL_UNPOOLED: source.DATABASE_URL_UNPOOLED,
527
- });
528
- if (pg.success) {
529
- result.postgres = {
530
- databaseUrl: pg.data.DATABASE_URL,
531
- databaseUrlUnpooled: pg.data.DATABASE_URL_UNPOOLED,
532
- } satisfies NeonPostgresEnv;
533
- } else {
534
- for (const issue of pg.error.issues) issues.push(issue.message);
535
- }
536
-
537
- if (desired.authEnabled) {
538
- const auth = authEnvSchema.safeParse({
539
- NEON_AUTH_BASE_URL: source.NEON_AUTH_BASE_URL,
540
- });
541
- if (auth.success) {
542
- result.auth = {
543
- baseUrl: auth.data.NEON_AUTH_BASE_URL,
544
- } satisfies NeonAuthEnv;
545
- } else {
546
- for (const issue of auth.error.issues) issues.push(issue.message);
547
- }
548
- }
549
-
550
- if (desired.dataApiEnabled) {
551
- const dataApi = dataApiEnvSchema.safeParse({
552
- NEON_DATA_API_URL: source.NEON_DATA_API_URL,
553
- });
554
- if (dataApi.success) {
555
- result.dataApi = {
556
- url: dataApi.data.NEON_DATA_API_URL,
557
- } satisfies NeonDataApiEnv;
558
- } else {
559
- for (const issue of dataApi.error.issues)
560
- issues.push(issue.message);
561
- }
562
- }
563
-
564
- if (issues.length > 0) {
565
- throw new PlatformError(
566
- ErrorCode.EnvNotInjected,
567
- [
568
- "parseEnv: the required Neon env variables are not present in process.env.",
569
- ...issues.map((i) => ` - ${i}`),
570
- "Inject them via one of:",
571
- " - `neon-env run -- <your dev command>` (wraps the command with the vars injected)",
572
- " - your hosting platform's Neon integration (Vercel, Fly, Railway, …)",
573
- "Or switch the call to `await fetchEnv(config)` if you're in a context that can do async I/O.",
574
- ].join("\n"),
575
- { details: { missing: issues } },
576
- );
577
- }
578
-
579
- return result as NeonEnv<C>;
580
- }
581
-
582
- // ───────────────────────── env-var mapping helpers ─────────────────────────
583
-
584
- /**
585
- * Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
586
- * for cross-process transport. Named after the web-platform `.entries()` convention
587
- * (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
588
- * iterator of tuples since that's the shape env injection needs (wrap with
589
- * `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
590
- * to inject the vars into a subprocess's `process.env`.
591
- *
592
- * Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
593
- * conditional namespaces are present.
594
- */
595
- export function toEntries(env: NeonEnv<Config>): Record<string, string> {
596
- const out: Record<string, string> = {
597
- [NEON_ENV_VAR_KEYS.postgres.databaseUrl]: env.postgres.databaseUrl,
598
- [NEON_ENV_VAR_KEYS.postgres.databaseUrlUnpooled]:
599
- env.postgres.databaseUrlUnpooled,
600
- };
601
- const withAuth = env as { auth?: NeonAuthEnv };
602
- if (withAuth.auth) {
603
- out[NEON_ENV_VAR_KEYS.auth.baseUrl] = withAuth.auth.baseUrl;
604
- }
605
- const withDataApi = env as { dataApi?: NeonDataApiEnv };
606
- if (withDataApi.dataApi) {
607
- out[NEON_ENV_VAR_KEYS.dataApi.url] = withDataApi.dataApi.url;
608
- }
609
- return out;
610
- }