neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
|
@@ -1,558 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Neon env core — resolving a branch's env from the Neon API, and projecting it into
|
|
3
|
-
* OS-level `{ KEY: value }` pairs.
|
|
4
|
-
*
|
|
5
|
-
* Shared source, not a package: `scripts/sync-shared.mjs` copies it into `@neon/env` (which
|
|
6
|
-
* publishes it as `fetchEnv` / `toEntries`) and into the `neon` CLI (which needs the
|
|
7
|
-
* credential-reuse half in `reuse-secrets.ts`). See `shared/env-core/README.md` for why it is
|
|
8
|
-
* a copy rather than an import.
|
|
9
|
-
*
|
|
10
|
-
* The counterpart that reads `process.env` — `parseEnv` and its zod schemas — is not here. It
|
|
11
|
-
* has no consumer outside `@neon/env`, so it stays in that package and imports this.
|
|
12
|
-
*/
|
|
13
|
-
import { createNeonApiFromOptions, deriveCredentialScopes, ErrorCode, PlatformError, resolveConfig, } from "@neon/config/v1";
|
|
14
|
-
/**
|
|
15
|
-
* Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
|
|
16
|
-
* for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
|
|
17
|
-
* that talks to `process.env`).
|
|
18
|
-
*
|
|
19
|
-
* Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
|
|
20
|
-
* camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
|
|
21
|
-
* by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
|
|
22
|
-
* {@link dataApiEnvSchema}.
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* Neon's default branch owner role, created with every project. This is the role a
|
|
26
|
-
* `DATABASE_URL` should connect as.
|
|
27
|
-
*/
|
|
28
|
-
const NEON_DEFAULT_OWNER_ROLE = "neondb_owner";
|
|
29
|
-
/**
|
|
30
|
-
* Neon's default database, created with every project. When a branch has several databases
|
|
31
|
-
* and none was requested, this is preferred for the `DATABASE_URL` so the common case (a
|
|
32
|
-
* user added a second database next to `neondb`) auto-picks without asking.
|
|
33
|
-
*/
|
|
34
|
-
const NEON_DEFAULT_DATABASE = "neondb";
|
|
35
|
-
/**
|
|
36
|
-
* Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
|
|
37
|
-
* RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
|
|
38
|
-
* so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
|
|
39
|
-
* Data API (`neon config apply`) adds these next to the owner role, which is why a plain
|
|
40
|
-
* branch routinely reports more than one role.
|
|
41
|
-
*/
|
|
42
|
-
const NEON_MANAGED_AUTH_ROLES = new Set([
|
|
43
|
-
"authenticator",
|
|
44
|
-
"anonymous",
|
|
45
|
-
"authenticated",
|
|
46
|
-
]);
|
|
47
|
-
export const NEON_ENV_VAR_KEYS = {
|
|
48
|
-
/**
|
|
49
|
-
* Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
|
|
50
|
-
* Neon Functions runtime on every branch (including the default) by default. `env pull` /
|
|
51
|
-
* `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
|
|
52
|
-
*/
|
|
53
|
-
branch: {
|
|
54
|
-
name: "NEON_BRANCH",
|
|
55
|
-
},
|
|
56
|
-
postgres: {
|
|
57
|
-
databaseUrl: "DATABASE_URL",
|
|
58
|
-
databaseUrlUnpooled: "DATABASE_URL_UNPOOLED",
|
|
59
|
-
},
|
|
60
|
-
auth: {
|
|
61
|
-
baseUrl: "NEON_AUTH_BASE_URL",
|
|
62
|
-
jwksUrl: "NEON_AUTH_JWKS_URL",
|
|
63
|
-
},
|
|
64
|
-
dataApi: {
|
|
65
|
-
url: "NEON_DATA_API_URL",
|
|
66
|
-
},
|
|
67
|
-
/**
|
|
68
|
-
* Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
|
|
69
|
-
* a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
|
|
70
|
-
* `region` is injected under the SDK-standard `AWS_REGION`.
|
|
71
|
-
*/
|
|
72
|
-
storage: {
|
|
73
|
-
accessKeyId: "AWS_ACCESS_KEY_ID",
|
|
74
|
-
secretAccessKey: "AWS_SECRET_ACCESS_KEY",
|
|
75
|
-
endpoint: "AWS_ENDPOINT_URL_S3",
|
|
76
|
-
region: "AWS_REGION",
|
|
77
|
-
},
|
|
78
|
-
/**
|
|
79
|
-
* AI Gateway (Preview). Exposed under the Neon-branded env vars the deployed Functions
|
|
80
|
-
* runtime injects: `apiKey` is the minted credential's bearer (`NEON_AI_GATEWAY_TOKEN`)
|
|
81
|
-
* and `baseUrl` is the bare branch gateway host (`NEON_AI_GATEWAY_BASE_URL`,
|
|
82
|
-
* `scheme://host`, no path). Clients like `@neon/ai-sdk-provider` read these and append the
|
|
83
|
-
* dialect route (`/v1`, `/openai/v1`, `/anthropic/v1`) themselves (https://github.com/vercel/ai/pull/15997).
|
|
84
|
-
*/
|
|
85
|
-
aiGateway: {
|
|
86
|
-
apiKey: "NEON_AI_GATEWAY_TOKEN",
|
|
87
|
-
baseUrl: "NEON_AI_GATEWAY_BASE_URL",
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
export async function fetchEnv(config, options) {
|
|
91
|
-
return fetchEnvKeys(config, options, options.keys ?? null);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* The {@link fetchEnv} body, with the key selection as a plain argument and no generic
|
|
95
|
-
* narrowing. Exists for callers that compute the selection at runtime — notably
|
|
96
|
-
* {@link fetchEnvReusingSecrets}, which decides which keys it still needs by checking the
|
|
97
|
-
* branch — since the public overload's `keys` is bound to a literal union those callers cannot
|
|
98
|
-
* produce without asserting.
|
|
99
|
-
*
|
|
100
|
-
* `keys === null` selects everything the policy enables.
|
|
101
|
-
*/
|
|
102
|
-
export async function fetchEnvKeys(config, options, keys) {
|
|
103
|
-
const api = options.api ?? createApiFromOptions(options);
|
|
104
|
-
const projectId = options.projectId;
|
|
105
|
-
const { branch, desired } = await resolveBranchPolicy(config, options, api);
|
|
106
|
-
const selection = keys ? new Set(keys) : null;
|
|
107
|
-
const wants = (key) => selection === null || selection.has(key);
|
|
108
|
-
const result = {};
|
|
109
|
-
const [roles, databases] = await Promise.all([
|
|
110
|
-
api.listBranchRoles(projectId, branch.id),
|
|
111
|
-
api.listBranchDatabases(projectId, branch.id),
|
|
112
|
-
]);
|
|
113
|
-
const roleName = pickRoleName(roles, branch, options.roleName);
|
|
114
|
-
const databaseName = pickDatabaseName(databases, branch, options.databaseName);
|
|
115
|
-
// Fan out: always fetch both Postgres URIs — the direct one also derives the AI Gateway
|
|
116
|
-
// host, so a selection that drops `DATABASE_URL_UNPOOLED` still needs it. Conditionally
|
|
117
|
-
// fetch auth + dataApi based on the branch policy and the selection. Auth key fields are
|
|
118
|
-
// only returned at integration creation time; for Better Auth they may legitimately be
|
|
119
|
-
// empty, so they can come back as empty strings.
|
|
120
|
-
const K = NEON_ENV_VAR_KEYS;
|
|
121
|
-
const wantsAuth = desired.authEnabled && (wants(K.auth.baseUrl) || wants(K.auth.jwksUrl));
|
|
122
|
-
const wantsDataApi = desired.dataApiEnabled && wants(K.dataApi.url);
|
|
123
|
-
const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
|
|
124
|
-
api.getConnectionUri(projectId, {
|
|
125
|
-
branchId: branch.id,
|
|
126
|
-
databaseName,
|
|
127
|
-
roleName,
|
|
128
|
-
pooled: true,
|
|
129
|
-
}),
|
|
130
|
-
api.getConnectionUri(projectId, {
|
|
131
|
-
branchId: branch.id,
|
|
132
|
-
databaseName,
|
|
133
|
-
roleName,
|
|
134
|
-
pooled: false,
|
|
135
|
-
}),
|
|
136
|
-
wantsAuth
|
|
137
|
-
? api.getNeonAuth(projectId, branch.id)
|
|
138
|
-
: Promise.resolve(null),
|
|
139
|
-
wantsDataApi
|
|
140
|
-
? api.getNeonDataApi(projectId, branch.id, databaseName)
|
|
141
|
-
: Promise.resolve(null),
|
|
142
|
-
]);
|
|
143
|
-
const postgres = {};
|
|
144
|
-
if (wants(K.postgres.databaseUrl))
|
|
145
|
-
postgres.databaseUrl = pooled.uri;
|
|
146
|
-
if (wants(K.postgres.databaseUrlUnpooled)) {
|
|
147
|
-
postgres.databaseUrlUnpooled = unpooled.uri;
|
|
148
|
-
}
|
|
149
|
-
if (Object.keys(postgres).length > 0)
|
|
150
|
-
result.postgres = postgres;
|
|
151
|
-
// Branch identity, mirroring what the Functions runtime injects on every branch. Surfaced
|
|
152
|
-
// as `NEON_BRANCH` so local dev (`neon dev` / `neon-env run` / `env pull`) matches the
|
|
153
|
-
// deployed runtime. Uses the branch name.
|
|
154
|
-
if (wants(K.branch.name)) {
|
|
155
|
-
result.branch = { name: branch.name };
|
|
156
|
-
}
|
|
157
|
-
if (wantsAuth) {
|
|
158
|
-
if (!authSnapshot) {
|
|
159
|
-
throw new PlatformError(ErrorCode.NotFound, [
|
|
160
|
-
`fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`,
|
|
161
|
-
"Enable it via `apply(config, { projectId, branchId })` (or `npx neon …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false.",
|
|
162
|
-
].join(" "), {
|
|
163
|
-
details: { projectId, branchId: branch.id },
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
const auth = {};
|
|
167
|
-
if (wants(K.auth.baseUrl))
|
|
168
|
-
auth.baseUrl = authSnapshot.baseUrl ?? "";
|
|
169
|
-
if (wants(K.auth.jwksUrl))
|
|
170
|
-
auth.jwksUrl = authSnapshot.jwksUrl ?? "";
|
|
171
|
-
result.auth = auth;
|
|
172
|
-
}
|
|
173
|
-
if (wantsDataApi) {
|
|
174
|
-
if (!dataApiSnapshot) {
|
|
175
|
-
throw new PlatformError(ErrorCode.NotFound, [
|
|
176
|
-
`fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${databaseName}.`,
|
|
177
|
-
"Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false.",
|
|
178
|
-
].join(" "), {
|
|
179
|
-
details: {
|
|
180
|
-
projectId,
|
|
181
|
-
branchId: branch.id,
|
|
182
|
-
databaseName,
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
result.dataApi = { url: dataApiSnapshot.url };
|
|
187
|
-
}
|
|
188
|
-
// Object storage + AI Gateway (Preview). A single branch credential backs whichever of
|
|
189
|
-
// these the policy enables; functions never force one but ride along on its scopes. None
|
|
190
|
-
// of this runs when the policy enables neither, so the Postgres / Auth / Data API path
|
|
191
|
-
// never touches the credentials/storage endpoints (and keeps working on production, where
|
|
192
|
-
// they may not exist yet).
|
|
193
|
-
const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
|
|
194
|
-
const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
|
|
195
|
-
const wantsStorage = storageEnabled &&
|
|
196
|
-
(wants(K.storage.accessKeyId) ||
|
|
197
|
-
wants(K.storage.secretAccessKey) ||
|
|
198
|
-
wants(K.storage.endpoint) ||
|
|
199
|
-
wants(K.storage.region));
|
|
200
|
-
const wantsGateway = gatewayEnabled &&
|
|
201
|
-
(wants(K.aiGateway.apiKey) || wants(K.aiGateway.baseUrl));
|
|
202
|
-
// A credential is minted only for its *secrets*. The endpoint, region and gateway host
|
|
203
|
-
// are plain branch metadata, so selecting only those touches no credential at all — which
|
|
204
|
-
// is how a caller holding valid secrets refreshes the rest without issuing a new one.
|
|
205
|
-
const wantsCredential = (storageEnabled &&
|
|
206
|
-
(wants(K.storage.accessKeyId) ||
|
|
207
|
-
wants(K.storage.secretAccessKey))) ||
|
|
208
|
-
(gatewayEnabled && wants(K.aiGateway.apiKey));
|
|
209
|
-
if (wantsStorage || wantsGateway) {
|
|
210
|
-
// Read the branch's storage settings *before* minting: a policy that declares buckets
|
|
211
|
-
// on a branch without storage has to fail without having spent a credential on a
|
|
212
|
-
// resolve that cannot succeed.
|
|
213
|
-
let storage = null;
|
|
214
|
-
if (wantsStorage) {
|
|
215
|
-
storage = await api.getProjectBranchStorage(projectId, branch.id);
|
|
216
|
-
if (!storage) {
|
|
217
|
-
throw new PlatformError(ErrorCode.NotFound, [
|
|
218
|
-
`fetchEnv: branch policy declares object storage (preview.buckets) but storage is not enabled on branch ${branch.name} (${branch.id}).`,
|
|
219
|
-
"Enable it via `apply(config, { projectId, branchId })` (or in the Neon Console) — then re-run fetchEnv. Or remove preview.buckets.",
|
|
220
|
-
].join(" "), { details: { projectId, branchId: branch.id } });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const secrets = wantsCredential
|
|
224
|
-
? await mintBranchCredential({
|
|
225
|
-
api,
|
|
226
|
-
projectId,
|
|
227
|
-
branchId: branch.id,
|
|
228
|
-
branchName: branch.name,
|
|
229
|
-
scopes: previewCredentialScopes(desired.preview),
|
|
230
|
-
})
|
|
231
|
-
: null;
|
|
232
|
-
if (storage) {
|
|
233
|
-
const storageEnv = {};
|
|
234
|
-
if (secrets && wants(K.storage.accessKeyId)) {
|
|
235
|
-
storageEnv.accessKeyId = secrets.accessKeyId;
|
|
236
|
-
}
|
|
237
|
-
if (secrets && wants(K.storage.secretAccessKey)) {
|
|
238
|
-
storageEnv.secretAccessKey = secrets.secretAccessKey;
|
|
239
|
-
}
|
|
240
|
-
if (wants(K.storage.endpoint)) {
|
|
241
|
-
storageEnv.endpoint = storage.s3Endpoint;
|
|
242
|
-
}
|
|
243
|
-
if (wants(K.storage.region))
|
|
244
|
-
storageEnv.region = storage.region;
|
|
245
|
-
result.storage = storageEnv;
|
|
246
|
-
}
|
|
247
|
-
if (wantsGateway) {
|
|
248
|
-
const gateway = {};
|
|
249
|
-
if (secrets && wants(K.aiGateway.apiKey)) {
|
|
250
|
-
gateway.apiKey = secrets.apiToken;
|
|
251
|
-
}
|
|
252
|
-
if (wants(K.aiGateway.baseUrl)) {
|
|
253
|
-
// Bare branch-scoped gateway host derived from the branch's connection URI —
|
|
254
|
-
// not the control-plane API origin (which doesn't serve the gateway). Clients
|
|
255
|
-
// append the dialect route (/v1, /openai/v1, /anthropic/v1) themselves.
|
|
256
|
-
gateway.baseUrl = aiGatewayBaseUrl(branch.id, unpooled.uri);
|
|
257
|
-
}
|
|
258
|
-
result.aiGateway = gateway;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Resolve the target branch and evaluate the policy against it — the first thing any
|
|
265
|
-
* branch-scoped operation needs. Shared by {@link fetchEnv} and {@link fetchEnvReusingSecrets}
|
|
266
|
-
* so the two agree on which branch they're talking about and what it has enabled.
|
|
267
|
-
*/
|
|
268
|
-
export async function resolveBranchPolicy(config, options, api) {
|
|
269
|
-
const projectId = options.projectId;
|
|
270
|
-
const branches = await api.listBranches(projectId);
|
|
271
|
-
if (branches.length === 0) {
|
|
272
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
273
|
-
`fetchEnv: project ${projectId} has no branches.`,
|
|
274
|
-
"Deploy your neon.ts policy (or create a branch) first, or pick a different project id.",
|
|
275
|
-
].join(" "), { details: { projectId } });
|
|
276
|
-
}
|
|
277
|
-
const branchRef = options.branch ?? options.branchId;
|
|
278
|
-
if (!branchRef) {
|
|
279
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
280
|
-
"fetchEnv: no branch provided.",
|
|
281
|
-
"Pass `branch` with a branch name (e.g. `main`) or id (`br-…`).",
|
|
282
|
-
].join(" "), { details: { projectId } });
|
|
283
|
-
}
|
|
284
|
-
const branch = resolveBranch(branchRef, branches);
|
|
285
|
-
const desired = resolveConfig(config, {
|
|
286
|
-
name: branch.name,
|
|
287
|
-
id: branch.id,
|
|
288
|
-
exists: true,
|
|
289
|
-
...(branch.parentId ? { parentId: branch.parentId } : {}),
|
|
290
|
-
isDefault: branch.isDefault,
|
|
291
|
-
isProtected: branch.protected,
|
|
292
|
-
...(branch.expiresAt ? { expiresAt: branch.expiresAt } : {}),
|
|
293
|
-
});
|
|
294
|
-
return { branch, desired };
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Scopes the branch credential should carry for a resolved branch policy. Only object storage
|
|
298
|
-
* and the AI Gateway *require* a credential; functions never force one (they have no credential
|
|
299
|
-
* of their own), but `functions:invoke` is added to the scope set when a credential is already
|
|
300
|
-
* being minted for storage / the AI Gateway, so the one credential can invoke the branch's
|
|
301
|
-
* functions too. Returns `[]` only when nothing credential-bearing is enabled.
|
|
302
|
-
*/
|
|
303
|
-
export function previewCredentialScopes(preview) {
|
|
304
|
-
if (!preview)
|
|
305
|
-
return [];
|
|
306
|
-
const storage = preview.buckets.length > 0;
|
|
307
|
-
const aiGateway = preview.aiGatewayEnabled;
|
|
308
|
-
if (!storage && !aiGateway)
|
|
309
|
-
return [];
|
|
310
|
-
return deriveCredentialScopes({
|
|
311
|
-
storage,
|
|
312
|
-
aiGateway,
|
|
313
|
-
functions: preview.functions.length > 0,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
/** The `name` this tool stamps on every credential it mints, so it can recognize its own. */
|
|
317
|
-
export function credentialName(branchName) {
|
|
318
|
-
return `neon-env ${branchName}`;
|
|
319
|
-
}
|
|
320
|
-
/** The env-var keys a branch credential's secrets surface under, in emit order. */
|
|
321
|
-
export function credentialEnvKeys(flags) {
|
|
322
|
-
return [
|
|
323
|
-
...(flags.storage
|
|
324
|
-
? [
|
|
325
|
-
NEON_ENV_VAR_KEYS.storage.accessKeyId,
|
|
326
|
-
NEON_ENV_VAR_KEYS.storage.secretAccessKey,
|
|
327
|
-
]
|
|
328
|
-
: []),
|
|
329
|
-
...(flags.aiGateway ? [NEON_ENV_VAR_KEYS.aiGateway.apiKey] : []),
|
|
330
|
-
];
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Every OS-level env var a resolved branch policy produces, in emit order. Lets a caller
|
|
334
|
-
* subtract the ones it already holds and pass the rest as {@link fetchEnv}'s `keys`, without
|
|
335
|
-
* re-deriving which vars a policy implies.
|
|
336
|
-
*/
|
|
337
|
-
export function policyEnvKeys(desired) {
|
|
338
|
-
const K = NEON_ENV_VAR_KEYS;
|
|
339
|
-
return [
|
|
340
|
-
K.postgres.databaseUrl,
|
|
341
|
-
K.postgres.databaseUrlUnpooled,
|
|
342
|
-
K.branch.name,
|
|
343
|
-
...(desired.authEnabled ? [K.auth.baseUrl, K.auth.jwksUrl] : []),
|
|
344
|
-
...(desired.dataApiEnabled ? [K.dataApi.url] : []),
|
|
345
|
-
...((desired.preview?.buckets.length ?? 0) > 0
|
|
346
|
-
? [
|
|
347
|
-
K.storage.accessKeyId,
|
|
348
|
-
K.storage.secretAccessKey,
|
|
349
|
-
K.storage.endpoint,
|
|
350
|
-
K.storage.region,
|
|
351
|
-
]
|
|
352
|
-
: []),
|
|
353
|
-
...(desired.preview?.aiGatewayEnabled
|
|
354
|
-
? [K.aiGateway.apiKey, K.aiGateway.baseUrl]
|
|
355
|
-
: []),
|
|
356
|
-
];
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Mint the branch credential backing object storage / the AI Gateway.
|
|
360
|
-
*
|
|
361
|
-
* `api_token` and `s3_secret_access_key` come back **exactly once** — they are not stored
|
|
362
|
-
* server-side and the list endpoint returns metadata only — so the caller's copy is the only
|
|
363
|
-
* copy. That is why {@link fetchEnv} mints rather than fetches: there is nothing to fetch. A
|
|
364
|
-
* caller that already holds a valid copy should leave the secret keys out of `keys` (see
|
|
365
|
-
* {@link fetchEnvReusingSecrets}) instead of minting one it will discard.
|
|
366
|
-
*/
|
|
367
|
-
async function mintBranchCredential(args) {
|
|
368
|
-
const minted = await args.api.createCredential(args.projectId, args.branchId, {
|
|
369
|
-
scopes: args.scopes,
|
|
370
|
-
principalType: "user",
|
|
371
|
-
name: credentialName(args.branchName),
|
|
372
|
-
});
|
|
373
|
-
return {
|
|
374
|
-
// The storage gateway authenticates against the full token id (e.g.
|
|
375
|
-
// `nak_live_…`), not the short token id — using the short id yields
|
|
376
|
-
// `InvalidAccessKeyId` on every S3 request.
|
|
377
|
-
accessKeyId: minted.tokenId,
|
|
378
|
-
secretAccessKey: minted.s3SecretAccessKey,
|
|
379
|
-
apiToken: minted.apiToken,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
|
|
384
|
-
* control-plane API origin. Derive the suffix from the branch's own Postgres connection host
|
|
385
|
-
* by dropping only the endpoint label (the first segment) and keeping everything after it,
|
|
386
|
-
* including any infra cell prefix (`c-N.`): a connection host of
|
|
387
|
-
* `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
|
|
388
|
-
* `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
|
|
389
|
-
* the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
|
|
390
|
-
*/
|
|
391
|
-
function aiGatewayHost(branchId, connectionUri) {
|
|
392
|
-
let connectionHost = "";
|
|
393
|
-
try {
|
|
394
|
-
connectionHost = new URL(connectionUri).hostname;
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
connectionHost = "";
|
|
398
|
-
}
|
|
399
|
-
// Drop the endpoint label (first segment, e.g. `ep-x` / `ep-x-pooler`), keeping the rest
|
|
400
|
-
// of the host verbatim — including any infra cell prefix (`c-N.`) the gateway routes on:
|
|
401
|
-
// `[c-N.]<region>.<cloud>.neon.<tld>`.
|
|
402
|
-
const suffix = connectionHost.split(".").slice(1).join(".");
|
|
403
|
-
return `${branchId}-api.ai.${suffix}`;
|
|
404
|
-
}
|
|
405
|
-
/** The AI Gateway's bare base URL (`NEON_AI_GATEWAY_BASE_URL`) on the branch gateway host. */
|
|
406
|
-
function aiGatewayBaseUrl(branchId, connectionUri) {
|
|
407
|
-
return `https://${aiGatewayHost(branchId, connectionUri)}`;
|
|
408
|
-
}
|
|
409
|
-
export function createApiFromOptions(options) {
|
|
410
|
-
return createNeonApiFromOptions("fetchEnv", {
|
|
411
|
-
...(options.apiKey ? { apiKey: options.apiKey } : {}),
|
|
412
|
-
...(options.apiHost ? { apiHost: options.apiHost } : {}),
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Resolve a branch ref — a name or an id — to a concrete branch. Matches by id first
|
|
417
|
-
* (exact `br-…`), then by name; both are unique within a project, so the lookup is
|
|
418
|
-
* unambiguous. This lets `.neon` files written by `neonctl` (which pin the branch *name*)
|
|
419
|
-
* and explicit `br-…` ids both work.
|
|
420
|
-
*/
|
|
421
|
-
function resolveBranch(branch, branches) {
|
|
422
|
-
const match = branches.find((b) => b.id === branch) ??
|
|
423
|
-
branches.find((b) => b.name === branch);
|
|
424
|
-
if (match)
|
|
425
|
-
return match;
|
|
426
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
427
|
-
`fetchEnv: branch ${JSON.stringify(branch)} not found on project (matched by id or name).`,
|
|
428
|
-
`Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`,
|
|
429
|
-
].join(" "), {
|
|
430
|
-
details: {
|
|
431
|
-
branch,
|
|
432
|
-
available: branches.map((b) => `${b.name} (${b.id})`),
|
|
433
|
-
},
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
function pickRoleName(roles, branch, requested) {
|
|
437
|
-
if (requested) {
|
|
438
|
-
if (!roles.some((r) => r.name === requested)) {
|
|
439
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
440
|
-
`fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`,
|
|
441
|
-
`Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`,
|
|
442
|
-
].join(" "), {
|
|
443
|
-
details: {
|
|
444
|
-
branchId: branch.id,
|
|
445
|
-
roleName: requested,
|
|
446
|
-
availableRoles: roles.map((r) => r.name),
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
return requested;
|
|
451
|
-
}
|
|
452
|
-
if (roles.length === 0) {
|
|
453
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
454
|
-
`fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`,
|
|
455
|
-
"Create one via the Neon console or pass `roleName` explicitly.",
|
|
456
|
-
].join(" "), { details: { branchId: branch.id } });
|
|
457
|
-
}
|
|
458
|
-
if (roles.length === 1)
|
|
459
|
-
return roles[0].name;
|
|
460
|
-
// Multiple roles. Enabling Neon Auth / the Data API provisions the PostgREST roles
|
|
461
|
-
// (authenticator/anonymous/authenticated) alongside the project owner, so a normal
|
|
462
|
-
// branch ends up with >1 role even though only the owner backs a `DATABASE_URL`.
|
|
463
|
-
// Default to Neon's owner role; if the project was created with a custom owner name,
|
|
464
|
-
// fall back to the single role left after dropping the managed auth roles. Only a
|
|
465
|
-
// genuinely ambiguous set (more than one app role) still asks the caller to choose.
|
|
466
|
-
const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
|
|
467
|
-
if (owner)
|
|
468
|
-
return owner.name;
|
|
469
|
-
const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
|
|
470
|
-
if (appRoles.length === 1)
|
|
471
|
-
return appRoles[0].name;
|
|
472
|
-
throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [
|
|
473
|
-
`fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles and none is "${NEON_DEFAULT_OWNER_ROLE}"; cannot auto-pick.`,
|
|
474
|
-
`Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`,
|
|
475
|
-
].join(" "), {
|
|
476
|
-
details: {
|
|
477
|
-
branchId: branch.id,
|
|
478
|
-
availableRoles: roles.map((r) => r.name),
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
function pickDatabaseName(databases, branch, requested) {
|
|
483
|
-
if (requested) {
|
|
484
|
-
if (!databases.some((d) => d.name === requested)) {
|
|
485
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
486
|
-
`fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`,
|
|
487
|
-
`Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`,
|
|
488
|
-
].join(" "), {
|
|
489
|
-
details: {
|
|
490
|
-
branchId: branch.id,
|
|
491
|
-
databaseName: requested,
|
|
492
|
-
availableDatabases: databases.map((d) => d.name),
|
|
493
|
-
},
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
return requested;
|
|
497
|
-
}
|
|
498
|
-
if (databases.length === 0) {
|
|
499
|
-
throw new PlatformError(ErrorCode.BranchNotFound, [
|
|
500
|
-
`fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`,
|
|
501
|
-
"Create one via the Neon console or pass `databaseName` explicitly.",
|
|
502
|
-
].join(" "), { details: { branchId: branch.id } });
|
|
503
|
-
}
|
|
504
|
-
// Prefer Neon's default `neondb`. On the common "added a second database" branch this
|
|
505
|
-
// auto-picks it, so a lone or `neondb`-including branch resolves without asking.
|
|
506
|
-
const neondb = databases.find((d) => d.name === NEON_DEFAULT_DATABASE);
|
|
507
|
-
if (neondb)
|
|
508
|
-
return neondb.name;
|
|
509
|
-
if (databases.length === 1)
|
|
510
|
-
return databases[0].name;
|
|
511
|
-
// Several databases and no `neondb` to fall back on. Auto-picking any of them would be
|
|
512
|
-
// perceived as random and is bad DX, so fail loudly and let the caller disambiguate.
|
|
513
|
-
throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [
|
|
514
|
-
`fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases and none is named "${NEON_DEFAULT_DATABASE}"; cannot auto-pick.`,
|
|
515
|
-
`Rename one to "${NEON_DEFAULT_DATABASE}" or keep a single database on the branch (or, when calling fetchEnv directly, pass \`databaseName\`). Available: ${databases.map((d) => d.name).join(", ")}.`,
|
|
516
|
-
].join(" "), {
|
|
517
|
-
details: {
|
|
518
|
-
branchId: branch.id,
|
|
519
|
-
availableDatabases: databases.map((d) => d.name),
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
// ───────────────────────── env-var mapping helpers ─────────────────────────
|
|
524
|
-
/**
|
|
525
|
-
* Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
|
|
526
|
-
* for cross-process transport. Named after the web-platform `.entries()` convention
|
|
527
|
-
* (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
|
|
528
|
-
* iterator of tuples since that's the shape env injection needs (wrap with
|
|
529
|
-
* `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
|
|
530
|
-
* to inject the vars into a subprocess's `process.env`.
|
|
531
|
-
*
|
|
532
|
-
* Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
|
|
533
|
-
* conditional namespaces are present.
|
|
534
|
-
*/
|
|
535
|
-
export function toEntries(env) {
|
|
536
|
-
const out = {};
|
|
537
|
-
const put = (key, value) => {
|
|
538
|
-
if (value !== undefined)
|
|
539
|
-
out[key] = value;
|
|
540
|
-
};
|
|
541
|
-
const K = NEON_ENV_VAR_KEYS;
|
|
542
|
-
put(K.postgres.databaseUrl, env.postgres?.databaseUrl);
|
|
543
|
-
put(K.postgres.databaseUrlUnpooled, env.postgres?.databaseUrlUnpooled);
|
|
544
|
-
put(K.branch.name, env.branch?.name);
|
|
545
|
-
put(K.auth.baseUrl, env.auth?.baseUrl);
|
|
546
|
-
put(K.auth.jwksUrl, env.auth?.jwksUrl);
|
|
547
|
-
put(K.dataApi.url, env.dataApi?.url);
|
|
548
|
-
put(K.storage.accessKeyId, env.storage?.accessKeyId);
|
|
549
|
-
put(K.storage.secretAccessKey, env.storage?.secretAccessKey);
|
|
550
|
-
put(K.storage.endpoint, env.storage?.endpoint);
|
|
551
|
-
put(K.storage.region, env.storage?.region);
|
|
552
|
-
// Neon-branded gateway vars only: the bearer and the bare branch gateway host
|
|
553
|
-
// (scheme://host, no path) — the @neon/ai-sdk-provider appends the dialect route
|
|
554
|
-
// (/v1, /openai/v1, /anthropic/v1) itself (https://github.com/vercel/ai/pull/15997).
|
|
555
|
-
put(K.aiGateway.apiKey, env.aiGateway?.apiKey);
|
|
556
|
-
put(K.aiGateway.baseUrl, env.aiGateway?.baseUrl);
|
|
557
|
-
return out;
|
|
558
|
-
}
|