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
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { NEON_SERVICES } from "../neon_services.js";
|
|
2
|
+
import { ErrorCode, PlatformError, createNeonApiFromOptions, deriveCredentialScopes, resolveConfig } from "@neon/config/v1";
|
|
3
|
+
//#region ../../internals/env-core/dist/env.js
|
|
4
|
+
/**
|
|
5
|
+
* The Neon env core — resolving a branch's env from the Neon API, and projecting it into
|
|
6
|
+
* OS-level `{ KEY: value }` pairs.
|
|
7
|
+
*
|
|
8
|
+
* Private, and bundled into both consumers: `@neon/env` publishes it as `fetchEnv` /
|
|
9
|
+
* `toEntries`, and the `neon` CLI needs the credential-reuse half in `reuse-secrets.ts`.
|
|
10
|
+
* See `README.md` for why it is not published.
|
|
11
|
+
*
|
|
12
|
+
* The counterpart that reads `process.env` — `parseEnv` and its zod schemas — is not here. It
|
|
13
|
+
* has no consumer outside `@neon/env`, so it stays in that package and imports this.
|
|
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
|
+
/**
|
|
26
|
+
* Neon's default branch owner role, created with every project. This is the role a
|
|
27
|
+
* `DATABASE_URL` should connect as.
|
|
28
|
+
*/
|
|
29
|
+
const NEON_DEFAULT_OWNER_ROLE = "neondb_owner";
|
|
30
|
+
/**
|
|
31
|
+
* Neon's default database, created with every project. When a branch has several databases
|
|
32
|
+
* and none was requested, this is preferred for the `DATABASE_URL` so the common case (a
|
|
33
|
+
* user added a second database next to `neondb`) auto-picks without asking.
|
|
34
|
+
*/
|
|
35
|
+
const NEON_DEFAULT_DATABASE = "neondb";
|
|
36
|
+
/**
|
|
37
|
+
* Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
|
|
38
|
+
* RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
|
|
39
|
+
* so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
|
|
40
|
+
* Data API (`neon config apply`) adds these next to the owner role, which is why a plain
|
|
41
|
+
* branch routinely reports more than one role.
|
|
42
|
+
*/
|
|
43
|
+
const NEON_MANAGED_AUTH_ROLES = /* @__PURE__ */ new Set([
|
|
44
|
+
"authenticator",
|
|
45
|
+
"anonymous",
|
|
46
|
+
"authenticated"
|
|
47
|
+
]);
|
|
48
|
+
const NEON_ENV_VAR_KEYS = {
|
|
49
|
+
/**
|
|
50
|
+
* Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
|
|
51
|
+
* Neon Functions runtime on every branch (including the default) by default. `env pull` /
|
|
52
|
+
* `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
|
|
53
|
+
*/
|
|
54
|
+
branch: { name: "NEON_BRANCH" },
|
|
55
|
+
postgres: {
|
|
56
|
+
databaseUrl: "DATABASE_URL",
|
|
57
|
+
databaseUrlUnpooled: "DATABASE_URL_UNPOOLED"
|
|
58
|
+
},
|
|
59
|
+
auth: {
|
|
60
|
+
baseUrl: "NEON_AUTH_BASE_URL",
|
|
61
|
+
jwksUrl: "NEON_AUTH_JWKS_URL"
|
|
62
|
+
},
|
|
63
|
+
dataApi: { url: "NEON_DATA_API_URL" },
|
|
64
|
+
/**
|
|
65
|
+
* Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
|
|
66
|
+
* a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
|
|
67
|
+
* `region` is injected under the SDK-standard `AWS_REGION`.
|
|
68
|
+
*/
|
|
69
|
+
storage: {
|
|
70
|
+
accessKeyId: "AWS_ACCESS_KEY_ID",
|
|
71
|
+
secretAccessKey: "AWS_SECRET_ACCESS_KEY",
|
|
72
|
+
endpoint: "AWS_ENDPOINT_URL_S3",
|
|
73
|
+
region: "AWS_REGION"
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* AI Gateway (Preview). Exposed under the Neon-branded env vars the deployed Functions
|
|
77
|
+
* runtime injects: `apiKey` is the minted credential's bearer (`NEON_AI_GATEWAY_TOKEN`)
|
|
78
|
+
* and `baseUrl` is the bare branch gateway host (`NEON_AI_GATEWAY_BASE_URL`,
|
|
79
|
+
* `scheme://host`, no path). Clients like `@neon/ai-sdk-provider` read these and append the
|
|
80
|
+
* dialect route (`/v1`, `/openai/v1`, `/anthropic/v1`) themselves (https://github.com/vercel/ai/pull/15997).
|
|
81
|
+
*/
|
|
82
|
+
aiGateway: {
|
|
83
|
+
apiKey: "NEON_AI_GATEWAY_TOKEN",
|
|
84
|
+
baseUrl: "NEON_AI_GATEWAY_BASE_URL"
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** Fail loudly when selected-key dependency planning and execution disagree. */
|
|
88
|
+
function requiredValue(value, description) {
|
|
89
|
+
if (value === null) throw new Error(`fetchEnv: missing ${description}.`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The {@link fetchEnv} body, with the key selection as a plain argument and no generic
|
|
94
|
+
* narrowing. Exists for callers that compute the selection at runtime — notably
|
|
95
|
+
* {@link fetchEnvReusingSecrets}, which decides which keys it still needs by checking the
|
|
96
|
+
* branch — since the public overload's `keys` is bound to a literal union those callers cannot
|
|
97
|
+
* produce without asserting.
|
|
98
|
+
*
|
|
99
|
+
* `keys === null` selects everything the policy enables.
|
|
100
|
+
*/
|
|
101
|
+
async function fetchEnvKeys(config, options, keys) {
|
|
102
|
+
const api = options.api ?? createApiFromOptions(options);
|
|
103
|
+
const projectId = options.projectId;
|
|
104
|
+
const { branch, desired } = await resolveBranchPolicy(config, options, api);
|
|
105
|
+
const selection = keys ? new Set(keys) : null;
|
|
106
|
+
const wants = (key) => selection === null || selection.has(key);
|
|
107
|
+
const result = {};
|
|
108
|
+
const K = NEON_ENV_VAR_KEYS;
|
|
109
|
+
const wantsPooled = wants(K.postgres.databaseUrl);
|
|
110
|
+
const wantsUnpooled = wants(K.postgres.databaseUrlUnpooled);
|
|
111
|
+
const wantsAuth = desired.authEnabled && (wants(K.auth.baseUrl) || wants(K.auth.jwksUrl));
|
|
112
|
+
const wantsDataApi = desired.dataApiEnabled && wants(K.dataApi.url);
|
|
113
|
+
const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
|
|
114
|
+
const needsUnpooled = wantsUnpooled || gatewayEnabled && wants(K.aiGateway.baseUrl);
|
|
115
|
+
const needsConnectionTarget = wantsPooled || needsUnpooled;
|
|
116
|
+
const needsDatabase = needsConnectionTarget || wantsDataApi;
|
|
117
|
+
const [roles, databases] = await Promise.all([needsConnectionTarget ? api.listBranchRoles(projectId, branch.id) : Promise.resolve([]), needsDatabase ? api.listBranchDatabases(projectId, branch.id) : Promise.resolve([])]);
|
|
118
|
+
const databaseName = needsDatabase ? pickDatabaseName(databases, branch, options.databaseName) : null;
|
|
119
|
+
const connectionTarget = needsConnectionTarget ? {
|
|
120
|
+
roleName: pickRoleName(roles, branch, options.roleName),
|
|
121
|
+
databaseName: requiredValue(databaseName, "database for a selected connection URI")
|
|
122
|
+
} : null;
|
|
123
|
+
const getConnectionUri = (pooled) => {
|
|
124
|
+
const target = requiredValue(connectionTarget, "role and database for a selected connection URI");
|
|
125
|
+
return api.getConnectionUri(projectId, {
|
|
126
|
+
branchId: branch.id,
|
|
127
|
+
...target,
|
|
128
|
+
pooled
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
|
|
132
|
+
wantsPooled ? getConnectionUri(true) : Promise.resolve(null),
|
|
133
|
+
needsUnpooled ? getConnectionUri(false) : Promise.resolve(null),
|
|
134
|
+
wantsAuth ? api.getNeonAuth(projectId, branch.id) : Promise.resolve(null),
|
|
135
|
+
wantsDataApi ? api.getNeonDataApi(projectId, branch.id, requiredValue(databaseName, "database for the selected Data API URL")) : Promise.resolve(null)
|
|
136
|
+
]);
|
|
137
|
+
const postgres = {};
|
|
138
|
+
if (wantsPooled) postgres.databaseUrl = requiredValue(pooled, "pooled connection URI response").uri;
|
|
139
|
+
if (wantsUnpooled) postgres.databaseUrlUnpooled = requiredValue(unpooled, "direct connection URI response").uri;
|
|
140
|
+
if (Object.keys(postgres).length > 0) result.postgres = postgres;
|
|
141
|
+
if (wants(K.branch.name)) result.branch = { name: branch.name };
|
|
142
|
+
if (wantsAuth) {
|
|
143
|
+
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 neon …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false."].join(" "), { details: {
|
|
144
|
+
projectId,
|
|
145
|
+
branchId: branch.id
|
|
146
|
+
} });
|
|
147
|
+
const auth = {};
|
|
148
|
+
if (wants(K.auth.baseUrl)) auth.baseUrl = authSnapshot.baseUrl ?? "";
|
|
149
|
+
if (wants(K.auth.jwksUrl)) auth.jwksUrl = authSnapshot.jwksUrl ?? "";
|
|
150
|
+
result.auth = auth;
|
|
151
|
+
}
|
|
152
|
+
if (wantsDataApi) {
|
|
153
|
+
if (!dataApiSnapshot) {
|
|
154
|
+
const selectedDatabase = requiredValue(databaseName, "database for the selected Data API URL");
|
|
155
|
+
throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${selectedDatabase}.`, "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false."].join(" "), { details: {
|
|
156
|
+
projectId,
|
|
157
|
+
branchId: branch.id,
|
|
158
|
+
databaseName: selectedDatabase
|
|
159
|
+
} });
|
|
160
|
+
}
|
|
161
|
+
result.dataApi = { url: dataApiSnapshot.url };
|
|
162
|
+
}
|
|
163
|
+
const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
|
|
164
|
+
const wantsStorage = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey) || wants(K.storage.endpoint) || wants(K.storage.region));
|
|
165
|
+
const wantsGateway = gatewayEnabled && (wants(K.aiGateway.apiKey) || wants(K.aiGateway.baseUrl));
|
|
166
|
+
const wantsStorageCredential = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey));
|
|
167
|
+
const wantsGatewayCredential = gatewayEnabled && wants(K.aiGateway.apiKey);
|
|
168
|
+
const wantsCredential = wantsStorageCredential || wantsGatewayCredential;
|
|
169
|
+
if (wantsStorage || wantsGateway) {
|
|
170
|
+
let storage = null;
|
|
171
|
+
if (wantsStorage) {
|
|
172
|
+
storage = await api.getProjectBranchStorage(projectId, branch.id);
|
|
173
|
+
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: {
|
|
174
|
+
projectId,
|
|
175
|
+
branchId: branch.id
|
|
176
|
+
} });
|
|
177
|
+
}
|
|
178
|
+
const secrets = wantsCredential ? await mintBranchCredential({
|
|
179
|
+
api,
|
|
180
|
+
projectId,
|
|
181
|
+
branchId: branch.id,
|
|
182
|
+
branchName: branch.name,
|
|
183
|
+
scopes: previewCredentialScopes(desired.preview, {
|
|
184
|
+
storage: wantsStorageCredential,
|
|
185
|
+
aiGateway: wantsGatewayCredential
|
|
186
|
+
})
|
|
187
|
+
}) : null;
|
|
188
|
+
if (storage) {
|
|
189
|
+
const storageEnv = {};
|
|
190
|
+
if (secrets && wants(K.storage.accessKeyId)) storageEnv.accessKeyId = secrets.accessKeyId;
|
|
191
|
+
if (secrets && wants(K.storage.secretAccessKey)) storageEnv.secretAccessKey = secrets.secretAccessKey;
|
|
192
|
+
if (wants(K.storage.endpoint)) storageEnv.endpoint = storage.s3Endpoint;
|
|
193
|
+
if (wants(K.storage.region)) storageEnv.region = storage.region;
|
|
194
|
+
result.storage = storageEnv;
|
|
195
|
+
}
|
|
196
|
+
if (wantsGateway) {
|
|
197
|
+
const gateway = {};
|
|
198
|
+
if (secrets && wants(K.aiGateway.apiKey)) gateway.apiKey = secrets.apiToken;
|
|
199
|
+
if (wants(K.aiGateway.baseUrl)) gateway.baseUrl = aiGatewayBaseUrl(branch.id, requiredValue(unpooled, "direct connection URI for the selected AI Gateway base URL").uri);
|
|
200
|
+
result.aiGateway = gateway;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the target branch and evaluate the policy against it — the first thing any
|
|
207
|
+
* branch-scoped operation needs. Shared by {@link fetchEnv} and {@link fetchEnvReusingSecrets}
|
|
208
|
+
* so the two agree on which branch they're talking about and what it has enabled.
|
|
209
|
+
*/
|
|
210
|
+
async function resolveBranchPolicy(config, options, api) {
|
|
211
|
+
const projectId = options.projectId;
|
|
212
|
+
const branches = await api.listBranches(projectId);
|
|
213
|
+
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 } });
|
|
214
|
+
const branchRef = options.branch ?? options.branchId;
|
|
215
|
+
if (!branchRef) throw new PlatformError(ErrorCode.BranchNotFound, ["fetchEnv: no branch provided.", "Pass `branch` with a branch name (e.g. `main`) or id (`br-…`)."].join(" "), { details: { projectId } });
|
|
216
|
+
const branch = resolveBranch(branchRef, branches);
|
|
217
|
+
return {
|
|
218
|
+
branch,
|
|
219
|
+
desired: resolveConfig(config, {
|
|
220
|
+
name: branch.name,
|
|
221
|
+
id: branch.id,
|
|
222
|
+
exists: true,
|
|
223
|
+
...branch.parentId ? { parentId: branch.parentId } : {},
|
|
224
|
+
isDefault: branch.isDefault,
|
|
225
|
+
isProtected: branch.protected,
|
|
226
|
+
...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Scopes the branch credential should carry for a resolved branch policy and optional key
|
|
232
|
+
* selection. Only object storage and the AI Gateway *require* a credential; functions never
|
|
233
|
+
* force one, but `functions:invoke` rides along when another selected feature mints one.
|
|
234
|
+
*/
|
|
235
|
+
function previewCredentialScopes(preview, selected) {
|
|
236
|
+
if (!preview) return [];
|
|
237
|
+
const storage = preview.buckets.length > 0 && (selected?.storage ?? true);
|
|
238
|
+
const aiGateway = preview.aiGatewayEnabled && (selected?.aiGateway ?? true);
|
|
239
|
+
if (!storage && !aiGateway) return [];
|
|
240
|
+
return deriveCredentialScopes({
|
|
241
|
+
storage,
|
|
242
|
+
aiGateway,
|
|
243
|
+
functions: preview.functions.length > 0
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** The `name` this tool stamps on every credential it mints, so it can recognize its own. */
|
|
247
|
+
function credentialName(branchName) {
|
|
248
|
+
return `neon-env ${branchName}`;
|
|
249
|
+
}
|
|
250
|
+
/** The env-var keys a branch credential's secrets surface under, in emit order. */
|
|
251
|
+
function credentialEnvKeys(flags) {
|
|
252
|
+
return [...flags.storage ? [NEON_ENV_VAR_KEYS.storage.accessKeyId, NEON_ENV_VAR_KEYS.storage.secretAccessKey] : [], ...flags.aiGateway ? [NEON_ENV_VAR_KEYS.aiGateway.apiKey] : []];
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Every OS-level env var a resolved branch policy produces, in emit order. Lets a caller
|
|
256
|
+
* subtract the ones it already holds and pass the rest as {@link fetchEnv}'s `keys`, without
|
|
257
|
+
* re-deriving which vars a policy implies.
|
|
258
|
+
*/
|
|
259
|
+
function policyEnvKeys(desired) {
|
|
260
|
+
const K = NEON_ENV_VAR_KEYS;
|
|
261
|
+
return [
|
|
262
|
+
K.postgres.databaseUrl,
|
|
263
|
+
K.postgres.databaseUrlUnpooled,
|
|
264
|
+
K.branch.name,
|
|
265
|
+
...desired.authEnabled ? [K.auth.baseUrl, K.auth.jwksUrl] : [],
|
|
266
|
+
...desired.dataApiEnabled ? [K.dataApi.url] : [],
|
|
267
|
+
...(desired.preview?.buckets.length ?? 0) > 0 ? [
|
|
268
|
+
K.storage.accessKeyId,
|
|
269
|
+
K.storage.secretAccessKey,
|
|
270
|
+
K.storage.endpoint,
|
|
271
|
+
K.storage.region
|
|
272
|
+
] : [],
|
|
273
|
+
...desired.preview?.aiGatewayEnabled ? [K.aiGateway.apiKey, K.aiGateway.baseUrl] : []
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Mint the branch credential backing object storage / the AI Gateway.
|
|
278
|
+
*
|
|
279
|
+
* `api_token` and `s3_secret_access_key` come back **exactly once** — they are not stored
|
|
280
|
+
* server-side and the list endpoint returns metadata only — so the caller's copy is the only
|
|
281
|
+
* copy. That is why {@link fetchEnv} mints rather than fetches: there is nothing to fetch. A
|
|
282
|
+
* caller that already holds a valid copy should leave the secret keys out of `keys` (see
|
|
283
|
+
* {@link fetchEnvReusingSecrets}) instead of minting one it will discard.
|
|
284
|
+
*/
|
|
285
|
+
async function mintBranchCredential(args) {
|
|
286
|
+
const minted = await args.api.createCredential(args.projectId, args.branchId, {
|
|
287
|
+
scopes: args.scopes,
|
|
288
|
+
principalType: "user",
|
|
289
|
+
name: credentialName(args.branchName)
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
accessKeyId: minted.tokenId,
|
|
293
|
+
secretAccessKey: minted.s3SecretAccessKey,
|
|
294
|
+
apiToken: minted.apiToken
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
|
|
299
|
+
* control-plane API origin. Derive the suffix from the branch's own Postgres connection host
|
|
300
|
+
* by dropping only the endpoint label (the first segment) and keeping everything after it,
|
|
301
|
+
* including any infra cell prefix (`c-N.`): a connection host of
|
|
302
|
+
* `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
|
|
303
|
+
* `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
|
|
304
|
+
* the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
|
|
305
|
+
*/
|
|
306
|
+
function aiGatewayHost(branchId, connectionUri) {
|
|
307
|
+
let connectionHost = "";
|
|
308
|
+
try {
|
|
309
|
+
connectionHost = new URL(connectionUri).hostname;
|
|
310
|
+
} catch {
|
|
311
|
+
connectionHost = "";
|
|
312
|
+
}
|
|
313
|
+
return `${branchId}-api.ai.${connectionHost.split(".").slice(1).join(".")}`;
|
|
314
|
+
}
|
|
315
|
+
/** The AI Gateway's bare base URL (`NEON_AI_GATEWAY_BASE_URL`) on the branch gateway host. */
|
|
316
|
+
function aiGatewayBaseUrl(branchId, connectionUri) {
|
|
317
|
+
return `https://${aiGatewayHost(branchId, connectionUri)}`;
|
|
318
|
+
}
|
|
319
|
+
function createApiFromOptions(options) {
|
|
320
|
+
return createNeonApiFromOptions("fetchEnv", {
|
|
321
|
+
...options.apiKey ? { apiKey: options.apiKey } : {},
|
|
322
|
+
...options.apiHost ? { apiHost: options.apiHost } : {}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Resolve a branch ref — a name or an id — to a concrete branch. Matches by id first
|
|
327
|
+
* (exact `br-…`), then by name; both are unique within a project, so the lookup is
|
|
328
|
+
* unambiguous. This lets `.neon` files written by `neonctl` (which pin the branch *name*)
|
|
329
|
+
* and explicit `br-…` ids both work.
|
|
330
|
+
*/
|
|
331
|
+
function resolveBranch(branch, branches) {
|
|
332
|
+
const match = branches.find((b) => b.id === branch) ?? branches.find((b) => b.name === branch);
|
|
333
|
+
if (match) return match;
|
|
334
|
+
throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${JSON.stringify(branch)} not found on project (matched by id or name).`, `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`].join(" "), { details: {
|
|
335
|
+
branch,
|
|
336
|
+
available: branches.map((b) => `${b.name} (${b.id})`)
|
|
337
|
+
} });
|
|
338
|
+
}
|
|
339
|
+
function pickRoleName(roles, branch, requested) {
|
|
340
|
+
if (requested) {
|
|
341
|
+
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: {
|
|
342
|
+
branchId: branch.id,
|
|
343
|
+
roleName: requested,
|
|
344
|
+
availableRoles: roles.map((r) => r.name)
|
|
345
|
+
} });
|
|
346
|
+
return requested;
|
|
347
|
+
}
|
|
348
|
+
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 } });
|
|
349
|
+
if (roles.length === 1) return roles[0].name;
|
|
350
|
+
const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
|
|
351
|
+
if (owner) return owner.name;
|
|
352
|
+
const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
|
|
353
|
+
if (appRoles.length === 1) return appRoles[0].name;
|
|
354
|
+
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: {
|
|
355
|
+
branchId: branch.id,
|
|
356
|
+
availableRoles: roles.map((r) => r.name)
|
|
357
|
+
} });
|
|
358
|
+
}
|
|
359
|
+
function pickDatabaseName(databases, branch, requested) {
|
|
360
|
+
if (requested) {
|
|
361
|
+
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: {
|
|
362
|
+
branchId: branch.id,
|
|
363
|
+
databaseName: requested,
|
|
364
|
+
availableDatabases: databases.map((d) => d.name)
|
|
365
|
+
} });
|
|
366
|
+
return requested;
|
|
367
|
+
}
|
|
368
|
+
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 } });
|
|
369
|
+
const neondb = databases.find((d) => d.name === NEON_DEFAULT_DATABASE);
|
|
370
|
+
if (neondb) return neondb.name;
|
|
371
|
+
if (databases.length === 1) return databases[0].name;
|
|
372
|
+
throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases and none is named "${NEON_DEFAULT_DATABASE}"; cannot auto-pick.`, `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(", ")}.`].join(" "), { details: {
|
|
373
|
+
branchId: branch.id,
|
|
374
|
+
availableDatabases: databases.map((d) => d.name)
|
|
375
|
+
} });
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
|
|
379
|
+
* for cross-process transport. Named after the web-platform `.entries()` convention
|
|
380
|
+
* (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
|
|
381
|
+
* iterator of tuples since that's the shape env injection needs (wrap with
|
|
382
|
+
* `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
|
|
383
|
+
* to inject the vars into a subprocess's `process.env`.
|
|
384
|
+
*
|
|
385
|
+
* Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
|
|
386
|
+
* conditional namespaces are present.
|
|
387
|
+
*/
|
|
388
|
+
function toEntries(env) {
|
|
389
|
+
const out = {};
|
|
390
|
+
const put = (key, value) => {
|
|
391
|
+
if (value !== void 0) out[key] = value;
|
|
392
|
+
};
|
|
393
|
+
const K = NEON_ENV_VAR_KEYS;
|
|
394
|
+
put(K.postgres.databaseUrl, env.postgres?.databaseUrl);
|
|
395
|
+
put(K.postgres.databaseUrlUnpooled, env.postgres?.databaseUrlUnpooled);
|
|
396
|
+
put(K.branch.name, env.branch?.name);
|
|
397
|
+
put(K.auth.baseUrl, env.auth?.baseUrl);
|
|
398
|
+
put(K.auth.jwksUrl, env.auth?.jwksUrl);
|
|
399
|
+
put(K.dataApi.url, env.dataApi?.url);
|
|
400
|
+
put(K.storage.accessKeyId, env.storage?.accessKeyId);
|
|
401
|
+
put(K.storage.secretAccessKey, env.storage?.secretAccessKey);
|
|
402
|
+
put(K.storage.endpoint, env.storage?.endpoint);
|
|
403
|
+
put(K.storage.region, env.storage?.region);
|
|
404
|
+
put(K.aiGateway.apiKey, env.aiGateway?.apiKey);
|
|
405
|
+
put(K.aiGateway.baseUrl, env.aiGateway?.baseUrl);
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/env_services.ts
|
|
410
|
+
/**
|
|
411
|
+
* The services `env pull --service` can select: every Neon service that produces branch env
|
|
412
|
+
* vars. `functions` is the one left out — a function's env comes from the local `neon.ts`,
|
|
413
|
+
* never from the branch, so there is nothing to pull.
|
|
414
|
+
*/
|
|
415
|
+
const ENV_PULL_SERVICES = NEON_SERVICES.filter((service) => service !== "functions");
|
|
416
|
+
/** Why the services `env pull` leaves out are not selectable, for the refusal message. */
|
|
417
|
+
const ENV_PULL_UNAVAILABLE = { functions: "a function's env comes from your neon.ts, not from the branch, so there is nothing to pull" };
|
|
418
|
+
/** Every OS-level env var `env pull` can write, in stable emit order. */
|
|
419
|
+
const ENV_PULL_KEYS = [
|
|
420
|
+
...Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
421
|
+
NEON_ENV_VAR_KEYS.branch.name,
|
|
422
|
+
...Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
423
|
+
...Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
424
|
+
...Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
425
|
+
...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
|
|
426
|
+
];
|
|
427
|
+
/** The OS-level env vars each service contributes to a pulled `.env`. */
|
|
428
|
+
const SERVICE_ENV_KEYS = {
|
|
429
|
+
postgres: Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
430
|
+
auth: Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
431
|
+
"data-api": Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
432
|
+
"object-storage": Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
433
|
+
"ai-gateway": Object.values(NEON_ENV_VAR_KEYS.aiGateway),
|
|
434
|
+
functions: []
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* The subset of {@link SERVICE_ENV_KEYS} a pull *owns*, and so may prune from the target file
|
|
438
|
+
* when the branch no longer has it. Object storage is deliberately absent: it is emitted under
|
|
439
|
+
* the third-party `AWS_*` names, which collide with credentials a user may set by hand, so
|
|
440
|
+
* `env pull` only ever writes them.
|
|
441
|
+
*/
|
|
442
|
+
const SERVICE_OWNED_ENV_KEYS = {
|
|
443
|
+
...SERVICE_ENV_KEYS,
|
|
444
|
+
"object-storage": []
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Branch identity. Not a service — every branch has a name — so a scoped pull refreshes it
|
|
448
|
+
* alongside whatever services were selected.
|
|
449
|
+
*/
|
|
450
|
+
const BRANCH_ENV_KEY = NEON_ENV_VAR_KEYS.branch.name;
|
|
451
|
+
/** The service that produces a key, or `null` for branch identity. */
|
|
452
|
+
const ENV_KEY_SERVICE = {
|
|
453
|
+
DATABASE_URL: "postgres",
|
|
454
|
+
DATABASE_URL_UNPOOLED: "postgres",
|
|
455
|
+
NEON_BRANCH: null,
|
|
456
|
+
NEON_AUTH_BASE_URL: "auth",
|
|
457
|
+
NEON_AUTH_JWKS_URL: "auth",
|
|
458
|
+
NEON_DATA_API_URL: "data-api",
|
|
459
|
+
AWS_ACCESS_KEY_ID: "object-storage",
|
|
460
|
+
AWS_SECRET_ACCESS_KEY: "object-storage",
|
|
461
|
+
AWS_ENDPOINT_URL_S3: "object-storage",
|
|
462
|
+
AWS_REGION: "object-storage",
|
|
463
|
+
NEON_AI_GATEWAY_TOKEN: "ai-gateway",
|
|
464
|
+
NEON_AI_GATEWAY_BASE_URL: "ai-gateway"
|
|
465
|
+
};
|
|
466
|
+
const serviceForEnvKey = (key) => ENV_KEY_SERVICE[key];
|
|
467
|
+
/** Services that must be resolved to produce the selected env keys. */
|
|
468
|
+
const servicesForEnvKeys = (keys) => ENV_PULL_SERVICES.filter((service) => keys.some((key) => ENV_KEY_SERVICE[key] === service));
|
|
469
|
+
/** Every env var the selected services contribute, plus branch identity. */
|
|
470
|
+
const envServiceKeys = (services) => {
|
|
471
|
+
const keys = /* @__PURE__ */ new Set([BRANCH_ENV_KEY]);
|
|
472
|
+
for (const service of services) for (const key of SERVICE_ENV_KEYS[service]) keys.add(key);
|
|
473
|
+
return keys;
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* The env vars a pull scoped to `services` may prune. Narrower than the unscoped set on
|
|
477
|
+
* purpose: `env pull -s ai-gateway` says nothing about `DATABASE_URL`, so it must leave it
|
|
478
|
+
* alone rather than treat its absence from this pull as "the branch no longer has it".
|
|
479
|
+
*/
|
|
480
|
+
const ownedEnvServiceKeys = (services) => services.flatMap((service) => SERVICE_OWNED_ENV_KEYS[service]);
|
|
481
|
+
/**
|
|
482
|
+
* The exact env vars an explicit selection writes. Services contribute their complete
|
|
483
|
+
* bundles plus branch identity; `--env` contributes only the named keys. The two selectors
|
|
484
|
+
* compose as a union.
|
|
485
|
+
*/
|
|
486
|
+
const envKeysForSelection = (services, envKeys) => {
|
|
487
|
+
const selected = services.length > 0 ? envServiceKeys(services) : /* @__PURE__ */ new Set();
|
|
488
|
+
for (const key of envKeys) selected.add(key);
|
|
489
|
+
if (selected.has(NEON_ENV_VAR_KEYS.storage.accessKeyId) !== selected.has(NEON_ENV_VAR_KEYS.storage.secretAccessKey)) throw new Error("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be selected together: they are two halves of one newly issued object-storage credential. Add the missing key to --env, or use --service object-storage.");
|
|
490
|
+
return ENV_PULL_KEYS.filter((key) => selected.has(key));
|
|
491
|
+
};
|
|
492
|
+
/** Parse repeated or comma-separated `--env` values into canonical env-key order. */
|
|
493
|
+
const parseEnvPullKeys = (raw, flag) => {
|
|
494
|
+
const names = raw.flatMap((value) => value.split(",")).map((name) => name.trim()).filter((name) => name !== "");
|
|
495
|
+
const supported = `Supported values: ${ENV_PULL_KEYS.join(", ")}.`;
|
|
496
|
+
if (names.length === 0) throw new Error(`${flag} needs at least one env variable. ${supported}`);
|
|
497
|
+
const unknown = names.filter((name) => !ENV_PULL_KEYS.some((key) => key === name));
|
|
498
|
+
if (unknown.length > 0) {
|
|
499
|
+
const displayNames = unknown.map(redactUnknownEnvValue);
|
|
500
|
+
const suggestions = [...new Set(unknown.map(suggestEnvPullKey).filter((key) => key !== null))];
|
|
501
|
+
const suggestion = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
502
|
+
throw new Error(`Unknown env variable${unknown.length === 1 ? "" : "s"} ${displayNames.join(", ")}.${suggestion} ${supported}`);
|
|
503
|
+
}
|
|
504
|
+
return ENV_PULL_KEYS.filter((key) => names.includes(key));
|
|
505
|
+
};
|
|
506
|
+
const redactUnknownEnvValue = (value) => {
|
|
507
|
+
const separator = value.indexOf("=");
|
|
508
|
+
if (separator !== -1) {
|
|
509
|
+
const key = value.slice(0, separator);
|
|
510
|
+
return ENV_PULL_KEYS.some((supportedKey) => supportedKey === key) ? `${key}=<redacted>` : "<redacted invalid value>";
|
|
511
|
+
}
|
|
512
|
+
return "<redacted invalid value>";
|
|
513
|
+
};
|
|
514
|
+
const suggestEnvPullKey = (value) => {
|
|
515
|
+
if (value.includes("=")) return null;
|
|
516
|
+
const closest = ENV_PULL_KEYS.map((key) => [key, editDistance(value, key)]).sort((a, b) => a[1] - b[1])[0];
|
|
517
|
+
return closest && closest[1] <= 2 ? closest[0] : null;
|
|
518
|
+
};
|
|
519
|
+
const editDistance = (left, right) => {
|
|
520
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
521
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
522
|
+
const current = [leftIndex];
|
|
523
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) current[rightIndex] = Math.min((previous[rightIndex] ?? 0) + 1, (current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
524
|
+
previous = current;
|
|
525
|
+
}
|
|
526
|
+
return previous[right.length] ?? right.length;
|
|
527
|
+
};
|
|
528
|
+
/** Narrow yargs' array option value without accepting any other runtime shape. */
|
|
529
|
+
const envPullFlagValue = (value) => Array.isArray(value) ? value.map(String) : void 0;
|
|
530
|
+
//#endregion
|
|
531
|
+
export { policyEnvKeys as _, envKeysForSelection as a, toEntries as b, ownedEnvServiceKeys as c, servicesForEnvKeys as d, NEON_ENV_VAR_KEYS as f, fetchEnvKeys as g, credentialName as h, ENV_PULL_UNAVAILABLE as i, parseEnvPullKeys as l, credentialEnvKeys as m, ENV_PULL_KEYS as n, envPullFlagValue as o, createApiFromOptions as p, ENV_PULL_SERVICES as r, envServiceKeys as s, BRANCH_ENV_KEY as t, serviceForEnvKey as u, previewCredentialScopes as v, resolveBranchPolicy as y };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
//#region ../../internals/cli-core/dist/paths.js
|
|
4
|
+
/**
|
|
5
|
+
* # Where the Neon CLIs keep their files on disk
|
|
6
|
+
*
|
|
7
|
+
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
8
|
+
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
9
|
+
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
10
|
+
* carry implementor-only code.
|
|
11
|
+
*
|
|
12
|
+
* It exists because three separate readers each grew their own answer to "where is the
|
|
13
|
+
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
14
|
+
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
15
|
+
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
16
|
+
* credentials somewhere the other two never looked.
|
|
17
|
+
*
|
|
18
|
+
* ## The directory
|
|
19
|
+
*
|
|
20
|
+
* `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
|
|
21
|
+
* each entry winning over the next:
|
|
22
|
+
*
|
|
23
|
+
* 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
|
|
24
|
+
* 2. `NEON_CONFIG_DIR` — exact.
|
|
25
|
+
* 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
|
|
26
|
+
* 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
|
|
27
|
+
*
|
|
28
|
+
* An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
|
|
29
|
+
* quietly read `~/.config/neonctl` would defeat the point of passing it.
|
|
30
|
+
*
|
|
31
|
+
* ## The files
|
|
32
|
+
*
|
|
33
|
+
* {@link resolveConfigFile} answers "which path should I use for this file", and it is the
|
|
34
|
+
* same answer for reading and writing:
|
|
35
|
+
*
|
|
36
|
+
* - Present in `neon/` → use it.
|
|
37
|
+
* - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
|
|
38
|
+
* never copied or moved, so nothing is left behind to go stale and no other tool starts
|
|
39
|
+
* reading an abandoned token.
|
|
40
|
+
* - Present in neither → the new location. New files only ever appear under `neon/`.
|
|
41
|
+
*/
|
|
42
|
+
/** Current directory name. New files are created here. */
|
|
43
|
+
const CONFIG_DIR_NAME = "neon";
|
|
44
|
+
/** Legacy directory name, read forever so existing installs keep working untouched. */
|
|
45
|
+
const LEGACY_CONFIG_DIR_NAME = "neonctl";
|
|
46
|
+
/** Where files are created. See the module docs for the precedence. */
|
|
47
|
+
function configDir(options = {}) {
|
|
48
|
+
const explicit = explicitDir(options);
|
|
49
|
+
if (explicit) return explicit;
|
|
50
|
+
return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The legacy directory, or `undefined` when the location was chosen explicitly (in which
|
|
54
|
+
* case there is no legacy counterpart to fall back to).
|
|
55
|
+
*/
|
|
56
|
+
function legacyConfigDir(options = {}) {
|
|
57
|
+
if (explicitDir(options)) return void 0;
|
|
58
|
+
return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve one file inside the config directory. Prefers the current location, falls back to
|
|
62
|
+
* an existing legacy file **in place**, and otherwise points at the current location so new
|
|
63
|
+
* files are created there.
|
|
64
|
+
*/
|
|
65
|
+
function resolveConfigFile(fileName, options = {}) {
|
|
66
|
+
const dir = configDir(options);
|
|
67
|
+
const current = resolve(dir, fileName);
|
|
68
|
+
if (existsSync(current)) return {
|
|
69
|
+
path: current,
|
|
70
|
+
dir,
|
|
71
|
+
isLegacy: false,
|
|
72
|
+
exists: true
|
|
73
|
+
};
|
|
74
|
+
const legacyDir = legacyConfigDir(options);
|
|
75
|
+
if (legacyDir) {
|
|
76
|
+
const legacy = resolve(legacyDir, fileName);
|
|
77
|
+
if (existsSync(legacy)) return {
|
|
78
|
+
path: legacy,
|
|
79
|
+
dir: legacyDir,
|
|
80
|
+
isLegacy: true,
|
|
81
|
+
exists: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
path: current,
|
|
86
|
+
dir,
|
|
87
|
+
isLegacy: false,
|
|
88
|
+
exists: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
|
|
92
|
+
function configHome(env) {
|
|
93
|
+
const xdg = nonEmpty(env.XDG_CONFIG_HOME);
|
|
94
|
+
if (xdg) return xdg;
|
|
95
|
+
const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
|
|
96
|
+
return home ? join(home, ".config") : ".config";
|
|
97
|
+
}
|
|
98
|
+
function explicitDir(options) {
|
|
99
|
+
const env = options.env ?? process.env;
|
|
100
|
+
return nonEmpty(options.dir) ?? nonEmpty(env.NEON_CONFIG_DIR) ?? nonEmpty(env.NEONCTL_CONFIG_DIR);
|
|
101
|
+
}
|
|
102
|
+
function nonEmpty(value) {
|
|
103
|
+
if (typeof value !== "string") return void 0;
|
|
104
|
+
const trimmed = value.trim();
|
|
105
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
106
|
+
}
|
|
107
|
+
const CREDENTIALS_FILE = "credentials.json";
|
|
108
|
+
/**
|
|
109
|
+
* Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
|
|
110
|
+
*
|
|
111
|
+
* The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
|
|
112
|
+
* see {@link credentialsPath} — but it is never written to, moved, or deleted.
|
|
113
|
+
*/
|
|
114
|
+
const defaultDir = configDir();
|
|
115
|
+
/**
|
|
116
|
+
* Where this invocation's `credentials.json` lives.
|
|
117
|
+
*
|
|
118
|
+
* When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
|
|
119
|
+
* directory is used **in place**: an install that predates the rename keeps working, and its
|
|
120
|
+
* credentials are never duplicated into a second location where one copy could go stale while
|
|
121
|
+
* another tool still reads it.
|
|
122
|
+
*
|
|
123
|
+
* A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
|
|
124
|
+
* explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
|
|
125
|
+
* scratch directory must never pick up a developer's real credentials.
|
|
126
|
+
*/
|
|
127
|
+
const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
|
|
128
|
+
/**
|
|
129
|
+
* Whether a credentials file is one the CLI created, rather than a path a profile adopted.
|
|
130
|
+
*
|
|
131
|
+
* Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
|
|
132
|
+
* that is what makes adopting an existing directory a one-line edit — and a file we did not
|
|
133
|
+
* create is not ours to remove.
|
|
134
|
+
*/
|
|
135
|
+
const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
|
|
136
|
+
/**
|
|
137
|
+
* Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
|
|
138
|
+
*
|
|
139
|
+
* {@link credentialsPath} deliberately reads an existing legacy file in place rather than
|
|
140
|
+
* migrating it, so for a default config directory that file is ours even though it sits outside
|
|
141
|
+
* `neon/`. Judging ownership on the current directory alone would call an install that predates
|
|
142
|
+
* the rename "adopted".
|
|
143
|
+
*/
|
|
144
|
+
const isOwnedCredentialPath = (configDirectory, file) => {
|
|
145
|
+
if (isInsideConfigDir(configDirectory, file)) return true;
|
|
146
|
+
if (configDirectory !== defaultDir) return false;
|
|
147
|
+
const legacy = legacyConfigDir();
|
|
148
|
+
return legacy !== void 0 && isInsideConfigDir(legacy, file);
|
|
149
|
+
};
|
|
150
|
+
//#endregion
|
|
151
|
+
export { isOwnedCredentialPath as a, isInsideConfigDir as i, credentialsPath as n, resolveConfigFile as o, defaultDir as r, CREDENTIALS_FILE as t };
|