neon 4.13.0 → 4.14.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 (41) hide show
  1. package/README.md +55 -13
  2. package/dist/_chunks/{credential_io-YeAxg9Xn.js → credential_io-D8AR-jvB.js} +4 -4
  3. package/dist/_chunks/{env-NbA61JR3.js → env-CZmstYrY.js} +71 -59
  4. package/dist/_chunks/{env_services-Tz9G4JeT.js → env_services-CAWrZTWa.js} +138 -41
  5. package/dist/_shared/auth_selection.js +83 -0
  6. package/dist/_shared/credentials.js +188 -0
  7. package/dist/_shared/paths.js +151 -0
  8. package/dist/_shared/profiles.js +226 -0
  9. package/dist/_shared/secure_file.js +41 -0
  10. package/dist/analytics.js +2 -2
  11. package/dist/auth.js +101 -11
  12. package/dist/auth_context.js +2 -11
  13. package/dist/claimable/api.js +2 -1
  14. package/dist/commands/ask.js +2 -1
  15. package/dist/commands/auth.js +155 -63
  16. package/dist/commands/checkout.js +28 -9
  17. package/dist/commands/claim.js +11 -7
  18. package/dist/commands/config.js +9 -9
  19. package/dist/commands/deploy.js +2 -1
  20. package/dist/commands/deploy_help.js +10 -0
  21. package/dist/commands/dev.js +147 -36
  22. package/dist/commands/env.js +33 -44
  23. package/dist/commands/functions.js +63 -2
  24. package/dist/commands/profile.js +2 -2
  25. package/dist/config_services.js +36 -1
  26. package/dist/credential_io.js +1 -1
  27. package/dist/custom_domains_api.js +37 -0
  28. package/dist/dev/env.js +1 -1
  29. package/dist/dev/functions.js +28 -11
  30. package/dist/dev/websocket.js +1 -1
  31. package/dist/env_services.js +2 -2
  32. package/dist/index.js +73 -20
  33. package/dist/init/build_config.js +4 -0
  34. package/dist/neon_services.js +2 -2
  35. package/dist/psql/wire/connection.js +1 -1
  36. package/dist/refresh_lock.js +44 -0
  37. package/dist/retire_credential.js +1 -1
  38. package/dist/test_utils/neon_api_server.js +60 -0
  39. package/dist/test_utils/rotating_oauth_server.js +94 -0
  40. package/dist/utils/config_diff.js +2 -13
  41. package/package.json +7 -5
@@ -1,19 +1,21 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { a as OAUTH, i as API_KEY, l as interpretCredentials, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-YeAxg9Xn.js";
2
+ import { a as OAUTH, i as API_KEY, l as interpretCredentials, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-D8AR-jvB.js";
3
3
  import { isCi } from "../env.js";
4
4
  import { log } from "../log.js";
5
- import { getApiClient } from "../api.js";
6
- import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
5
+ import { getApiClient, isNeonApiError } from "../api.js";
7
6
  import { setAuthContext } from "../auth_context.js";
7
+ import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
8
8
  import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
9
9
  import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-CvnFEQyd.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
- import { auth, refreshToken } from "../auth.js";
11
+ import { AuthRefreshError, auth, classifyRefreshFailure, refreshToken } from "../auth.js";
12
12
  import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
13
13
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
14
14
  import { assertionHasExpired, claimableCredentialsPath, readClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials } from "../claimable/state.js";
15
+ import { withExclusiveLock } from "../refresh_lock.js";
15
16
  import { readOutgoingCredential, retirePreviousCredential } from "../retire_credential.js";
16
17
  import { existsSync } from "node:fs";
18
+ import { join } from "node:path";
17
19
  import { createHash } from "node:crypto";
18
20
  //#region src/commands/auth.ts
19
21
  var auth_exports = /* @__PURE__ */ __exportAll({
@@ -25,7 +27,9 @@ var auth_exports = /* @__PURE__ */ __exportAll({
25
27
  describe: () => describe,
26
28
  ensureAuth: () => ensureAuth,
27
29
  handler: () => handler,
30
+ isAccessTokenUsable: () => isAccessTokenUsable,
28
31
  locationForAuth: () => locationForAuth,
32
+ refreshStoredCredentials: () => refreshStoredCredentials,
29
33
  usableCredential: () => usableCredential
30
34
  });
31
35
  const locationForAuth = (configDir, name, keyring, options = {}) => {
@@ -78,7 +82,7 @@ const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, "f
78
82
  });
79
83
  let identity = {};
80
84
  try {
81
- identity = await preserveCredentials(at, tokenSet, getApiClient({
85
+ identity = await persistNewSession(at, tokenSet, getApiClient({
82
86
  apiKey: tokenSet.access_token || "",
83
87
  apiHost
84
88
  }), configDir);
@@ -125,67 +129,136 @@ const authFlow = async ({ configDir, oauthHost, clientId, apiHost, forceAuth, "f
125
129
  return tokenSet.access_token || "";
126
130
  };
127
131
  /**
128
- * Persist the token set and return the account it belongs to, so a named profile can be
129
- * labelled with an email. The credentials file records only `user_id` — a UUID with no
130
- * email — which is why identifying a stored profile offline is otherwise impossible.
132
+ * Persist before identity lookup so a lookup failure cannot discard the new session.
133
+ * Merge the identity only while that session remains current on disk.
131
134
  */
132
- const preserveCredentials = async (at, credentials, apiClient, configDir) => {
133
- const { data: { id, email } } = await apiClient.getCurrentUserInfo();
134
- const stored = {
135
- ...credentials,
136
- type: OAUTH,
137
- ...id !== void 0 ? { user_id: id } : {}
138
- };
139
- storeFor(configDir).write(at, stored);
140
- log.debug("Saved credentials to %s", credentialLabel(at));
141
- log.debug("Credentials MD5 hash: %s", md5hash(JSON.stringify(stored)));
142
- return {
143
- ...id ? { id } : {},
144
- ...email ? { email } : {}
145
- };
135
+ const persistNewSession = async (at, credentials, apiClient, configDir) => {
136
+ const stored = oauthCredentialsFromTokenSet(credentials, null);
137
+ writeOAuthCredentials(configDir, at, stored);
138
+ const identity = await fetchIdentity(apiClient);
139
+ if (identity.id) {
140
+ const current = storeFor(configDir).read(at);
141
+ if (current !== null && current.credentials.access_token === stored.access_token) writeOAuthCredentials(configDir, at, {
142
+ ...current.credentials,
143
+ user_id: identity.id
144
+ });
145
+ }
146
+ return identity;
147
+ };
148
+ const fetchIdentity = async (apiClient) => {
149
+ try {
150
+ const { data: { id, email } } = await apiClient.getCurrentUserInfo();
151
+ return {
152
+ ...id ? { id } : {},
153
+ ...email ? { email } : {}
154
+ };
155
+ } catch (err) {
156
+ if (isNeonApiError(err) && err.status === 401) throw new Error("Signed in, but the Neon API rejected the new access token. Try `neon auth` again.");
157
+ log.warning("Signed in, but could not look up the account: %s", err instanceof Error ? err.message : String(err));
158
+ return {};
159
+ }
160
+ };
161
+ const isAccessTokenUsable = (credentials, now) => {
162
+ const token = credentials.access_token;
163
+ if (typeof token !== "string" || token === "") return false;
164
+ const expiresAt = credentials.expires_at;
165
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return false;
166
+ return expiresAt > now;
146
167
  };
147
168
  const handleExistingToken = async (tokenSet, props, at) => {
148
- if (tokenSet.access_token && tokenSet.expires_at > Date.now()) {
169
+ if (isAccessTokenUsable(tokenSet, Date.now())) {
149
170
  log.debug("Using existing valid access_token");
150
- const apiClient = getApiClient({
151
- apiKey: tokenSet.access_token,
152
- apiHost: props.apiHost
153
- });
171
+ const apiKey = tokenSet.access_token ?? "";
154
172
  return {
155
- apiKey: tokenSet.access_token,
156
- apiClient
173
+ apiKey,
174
+ apiClient: getApiClient({
175
+ apiKey,
176
+ apiHost: props.apiHost
177
+ }),
178
+ refreshed: false
157
179
  };
158
180
  }
159
- log.debug(tokenSet.expires_at < Date.now() ? "Token is expired, attempting refresh" : "Token is missing access_token, attempting refresh");
160
- if (!tokenSet.refresh_token) {
161
- log.debug("TokenSet is missing refresh_token, starting authentication");
181
+ if (typeof tokenSet.refresh_token !== "string" || tokenSet.refresh_token === "") {
182
+ log.debug("Stored credentials hold no refresh_token");
162
183
  return null;
163
184
  }
164
- let refreshedTokenSet;
165
- try {
166
- refreshedTokenSet = extendTokenSet(await refreshToken({
167
- oauthHost: props.oauthHost,
168
- clientId: props.clientId,
169
- allowUnsafeTls: props.allowUnsafeTls
170
- }, tokenSet));
171
- } catch (err) {
172
- const typedErr = err instanceof Error ? err : /* @__PURE__ */ new Error("Unknown error");
173
- log.debug("Failed to refresh token: %s", typedErr.message);
174
- throw new Error("AUTH_REFRESH_FAILED");
175
- }
176
- const apiKey = refreshedTokenSet.access_token;
177
- const apiClient = getApiClient({
178
- apiKey,
179
- apiHost: props.apiHost
180
- });
181
- await preserveCredentials(at, refreshedTokenSet, apiClient, props.configDir);
182
- log.debug("Token refresh successful");
185
+ log.debug("Access token is missing or expired, attempting refresh");
186
+ const next = await performRefresh(tokenSet, props, at);
187
+ const apiKey = next.credentials.access_token ?? "";
183
188
  return {
184
189
  apiKey,
185
- apiClient
190
+ apiClient: getApiClient({
191
+ apiKey,
192
+ apiHost: props.apiHost
193
+ }),
194
+ refreshed: next.rotated
186
195
  };
187
196
  };
188
197
  /**
198
+ * Serialize exchanges because presenting a one-time refresh token twice can revoke
199
+ * the newly issued token family. Persist immediately because the presented token is
200
+ * invalid once the server answers.
201
+ */
202
+ const performRefresh = async (credentials, props, at) => {
203
+ return await withExclusiveLock(refreshLockPath(props.configDir, at), async () => {
204
+ const loaded = storeFor(props.configDir).read(at);
205
+ const current = loaded?.credentials ?? credentials;
206
+ if (loaded !== null && isAccessTokenUsable(current, Date.now()) && current.access_token !== credentials.access_token) {
207
+ log.debug("Refresh lost a race; adopting the credentials another command wrote");
208
+ return {
209
+ credentials: current,
210
+ rotated: false
211
+ };
212
+ }
213
+ let refreshed;
214
+ try {
215
+ refreshed = extendTokenSet(await refreshToken({
216
+ oauthHost: props.oauthHost,
217
+ clientId: props.clientId,
218
+ allowUnsafeTls: props.allowUnsafeTls
219
+ }, current));
220
+ } catch (err) {
221
+ throw err instanceof AuthRefreshError ? err : classifyRefreshFailure(err);
222
+ }
223
+ const next = oauthCredentialsFromTokenSet(refreshed, loaded?.credentials ?? credentials);
224
+ writeOAuthCredentials(props.configDir, at, next, { restorePrevious: false });
225
+ log.debug("Token refresh successful");
226
+ return {
227
+ credentials: next,
228
+ rotated: true
229
+ };
230
+ });
231
+ };
232
+ /** A 401 proves the access token is stale even before `expires_at`. */
233
+ const refreshStoredCredentials = async (at, props) => {
234
+ const loaded = storeFor(props.configDir).read(at);
235
+ if (loaded === null) return false;
236
+ if (interpretCredentials(loaded.credentials, at, loaded.backend).kind === "api_key") return false;
237
+ if (typeof loaded.credentials.refresh_token !== "string" || loaded.credentials.refresh_token === "") return false;
238
+ await performRefresh(loaded.credentials, props, at);
239
+ return true;
240
+ };
241
+ const oauthCredentialsFromTokenSet = (tokenSet, previous) => {
242
+ const refreshTokenValue = typeof tokenSet.refresh_token === "string" && tokenSet.refresh_token !== "" ? tokenSet.refresh_token : previous?.refresh_token;
243
+ const stored = {
244
+ ...tokenSet,
245
+ type: OAUTH,
246
+ expires_at: tokenSet.expires_at,
247
+ ...typeof refreshTokenValue === "string" && refreshTokenValue !== "" ? { refresh_token: refreshTokenValue } : {}
248
+ };
249
+ if (previous?.user_id !== void 0) stored.user_id = previous.user_id;
250
+ return stored;
251
+ };
252
+ const writeOAuthCredentials = (configDir, at, credentials, options) => {
253
+ storeFor(configDir).write(at, credentials, options);
254
+ log.debug("Saved credentials to %s", credentialLabel(at));
255
+ log.debug("Credentials MD5 hash: %s", md5hash(JSON.stringify(credentials)));
256
+ };
257
+ const refreshLockPath = (configDir, at) => {
258
+ const id = createHash("sha256").update(credentialLabel(at)).digest("hex").slice(0, 16);
259
+ return join(configDir, `.refresh-${id}.lock`);
260
+ };
261
+ /**
189
262
  * The credential a stored file can authenticate with right now — its API key, or an OAuth
190
263
  * access token, refreshed first when it has expired. Never launches a browser.
191
264
  *
@@ -316,6 +389,17 @@ const ensureAuth = async (props) => {
316
389
  return;
317
390
  }
318
391
  try {
392
+ setAuthContext({
393
+ source: "stored-credentials",
394
+ configDir: props.configDir,
395
+ profile: selection.profile,
396
+ storage: at.storage,
397
+ accessToken: loaded.credentials.access_token,
398
+ refreshed: false,
399
+ oauthHost: props.oauthHost,
400
+ clientId: props.clientId,
401
+ ...at.storage === "file" ? { credentialsPath: at.path } : {}
402
+ });
319
403
  const result = await handleExistingToken(loaded.credentials, props, at);
320
404
  if (result) {
321
405
  props.apiKey = result.apiKey;
@@ -325,13 +409,23 @@ const ensureAuth = async (props) => {
325
409
  configDir: props.configDir,
326
410
  profile: selection.profile,
327
411
  storage: at.storage,
412
+ accessToken: result.apiKey,
413
+ refreshed: result.refreshed,
414
+ oauthHost: props.oauthHost,
415
+ clientId: props.clientId,
328
416
  ...at.storage === "file" ? { credentialsPath: at.path } : {}
329
417
  });
330
418
  return;
331
419
  }
332
420
  } catch (err) {
333
- if (!(err instanceof Error && err.message === "AUTH_REFRESH_FAILED")) throw err;
334
- log.debug("Ensure auth failed, starting authentication", err);
421
+ if (err instanceof AuthRefreshError) {
422
+ if (isLocalDev || isBootstrap || isMcp) {
423
+ log.warning("%s Continuing without credentials.", err.message);
424
+ return;
425
+ }
426
+ throw err;
427
+ }
428
+ throw err;
335
429
  }
336
430
  } else log.debug("No usable credentials at %s, starting authentication", credentialLabel(at));
337
431
  if (isLocalDev) {
@@ -360,18 +454,16 @@ const ensureAuth = async (props) => {
360
454
  configDir: props.configDir,
361
455
  profile: selection.profile,
362
456
  storage: at.storage,
457
+ accessToken: apiKey,
458
+ refreshed: true,
459
+ oauthHost: props.oauthHost,
460
+ clientId: props.clientId,
363
461
  ...at.storage === "file" ? { credentialsPath: at.path } : {}
364
462
  });
365
463
  };
366
464
  /**
367
- * Delete one credentials file — used by the 401 handler to clear an OAuth token set the API
368
- * has rejected, so the next command logs in again instead of failing the same way.
369
- *
370
- * It takes the exact path rather than a directory and an optional profile. Deriving the path
371
- * here meant re-running selection from partial state, and the 401 handler has no parsed
372
- * arguments: it passed only the config directory, so a rejected token on a
373
- * `--profile`-selected account deleted whatever `DEFAULT` pointed at — signing the user out
374
- * of an account whose credentials the failed request had never touched.
465
+ * Accept an exact location because deriving it from a config directory used to
466
+ * delete DEFAULT when a named profile failed.
375
467
  */
376
468
  const deleteCredentialsAt = (at, configDir) => {
377
469
  try {
@@ -388,4 +480,4 @@ const deleteCredentialsAt = (at, configDir) => {
388
480
  };
389
481
  const md5hash = (s) => createHash("md5").update(s).digest("hex");
390
482
  //#endregion
391
- export { aliases, authFlow, builder, command, deleteCredentialsAt, describe, ensureAuth, handler, locationForAuth, auth_exports as t, usableCredential };
483
+ export { aliases, authFlow, builder, command, deleteCredentialsAt, describe, ensureAuth, handler, isAccessTokenUsable, locationForAuth, refreshStoredCredentials, auth_exports as t, usableCredential };
@@ -8,7 +8,7 @@ import { looksLikeBranchId } from "../utils/formats.js";
8
8
  import { fillSingleProject } from "../utils/enrichers.js";
9
9
  import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
10
10
  import { autoPullEnvAfterPin } from "./env.js";
11
- import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout } from "./config.js";
11
+ import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, envFlag } from "./config.js";
12
12
  import { handler as handler$1 } from "./link.js";
13
13
  import chalk from "chalk";
14
14
  import prompts from "prompts";
@@ -17,10 +17,20 @@ var checkout_exports = /* @__PURE__ */ __exportAll({
17
17
  builder: () => builder,
18
18
  command: () => command,
19
19
  describe: () => describe,
20
+ formatCheckoutPolicyFailure: () => formatCheckoutPolicyFailure,
20
21
  handler: () => handler
21
22
  });
22
23
  const command = "checkout [id|name]";
23
24
  const describe = "Pin a branch in the local context (.neon) so subsequent commands target it";
25
+ const formatCheckoutPolicyFailure = (opts) => {
26
+ const cli = getCliName();
27
+ const envArg = opts.env ? ` --env ${opts.env}` : "";
28
+ return [
29
+ `Branch ${opts.branchName} (${opts.branchId}) was created and checked out, but applying neon.ts to it failed: ${opts.failure}`,
30
+ `The branch is usable but does not match the policy, and \`${cli} checkout\` never reconciles a branch that already exists.`,
31
+ `Fix the cause above, then run \`${cli} deploy --update-existing${envArg}\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${cli} branches delete ${opts.branchName}\` then \`${cli} checkout ${opts.branchName}${envArg}\`.`
32
+ ].join("\n");
33
+ };
24
34
  const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").positional("id", {
25
35
  describe: "Branch name or id to check out. Omit to pick interactively from the list of branches.",
26
36
  type: "string"
@@ -33,10 +43,16 @@ const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").position
33
43
  describe: "Pull the branch's Neon env vars (DATABASE_URL, …) into a local .env after checkout. On by default; use --no-env-pull to skip (e.g. when injecting env at runtime with `neon-env run` / `neon dev`).",
34
44
  type: "boolean",
35
45
  default: true
46
+ },
47
+ ...envFlag,
48
+ env: {
49
+ ...envFlag.env,
50
+ describe: envFlag.env.describe + " Used when this checkout creates a branch from neon.ts; ignored for an existing branch."
36
51
  }
37
52
  }).example([
38
53
  ["$0 checkout", "Pick a branch interactively from the project in the closest .neon file"],
39
54
  ["$0 checkout main", "Pin the branch named \"main\" in the closest .neon file"],
55
+ ["$0 checkout feat --env .env.local", "Create feat from neon.ts, loading Function env from .env.local first"],
40
56
  ["$0 checkout br-cool-snow-12345678 --project-id project-id-123", "Pin a branch by id for an explicit project"]
41
57
  ]);
42
58
  const handler = async (props) => {
@@ -56,7 +72,8 @@ const handler = async (props) => {
56
72
  branchId,
57
73
  ...props.apiKey ? { apiKey: props.apiKey } : {},
58
74
  ...props.apiHost ? { apiHost: props.apiHost } : {},
59
- ...props.color !== void 0 ? { color: props.color } : {}
75
+ ...props.color !== void 0 ? { color: props.color } : {},
76
+ ...props.env ? { env: props.env } : {}
60
77
  }) : policyFailure;
61
78
  await autoPullEnvAfterPin({
62
79
  ...props,
@@ -64,11 +81,12 @@ const handler = async (props) => {
64
81
  branch: branchId,
65
82
  envPull: props.envPull
66
83
  });
67
- if (failure) throw new Error([
68
- `Branch ${branchName} (${branchId}) was created and checked out, but applying neon.ts to it failed: ${failure}`,
69
- `The branch is usable but does not match the policy, and \`${getCliName()} checkout\` never reconciles a branch that already exists.`,
70
- `Fix the cause above, then run \`${getCliName()} deploy --update-existing\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${getCliName()} branches delete ${branchName}\` then \`${getCliName()} checkout ${branchName}\`.`
71
- ].join("\n"));
84
+ if (failure) throw new Error(formatCheckoutPolicyFailure({
85
+ branchName,
86
+ branchId,
87
+ failure,
88
+ ...props.env ? { env: props.env } : {}
89
+ }));
72
90
  };
73
91
  /**
74
92
  * Apply the policy to a branch `checkout` just created bare, returning the failure message
@@ -143,7 +161,8 @@ const createCheckoutBranch = async (props, projectId, name, branches) => {
143
161
  branchName: name,
144
162
  ...props.apiKey ? { apiKey: props.apiKey } : {},
145
163
  ...props.apiHost ? { apiHost: props.apiHost } : {},
146
- ...props.color !== void 0 ? { color: props.color } : {}
164
+ ...props.color !== void 0 ? { color: props.color } : {},
165
+ ...props.env ? { env: props.env } : {}
147
166
  });
148
167
  if (fromPolicy) return {
149
168
  branchId: fromPolicy.branchId,
@@ -233,4 +252,4 @@ const tryAutoDetectProject = async (props) => {
233
252
  }
234
253
  };
235
254
  //#endregion
236
- export { builder, command, describe, handler, checkout_exports as t };
255
+ export { builder, command, describe, formatCheckoutPolicyFailure, handler, checkout_exports as t };
@@ -2,7 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { applyContext, contextBranch, ensureGitignored, readContextFile } from "../context.js";
5
- import { declaredNeonServices } from "../config_services.js";
5
+ import { claimableDataApiCreateBody, declaredNeonServices } from "../config_services.js";
6
6
  import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
7
7
  import { mergeEnvFile, resolveEnvFilePath } from "../env_file.js";
8
8
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
@@ -80,11 +80,11 @@ const findNeonConfig = (cwd = process.cwd()) => {
80
80
  current = parent;
81
81
  }
82
82
  };
83
- const servicesFromConfig = async (explicitPath) => {
83
+ const loadCreatePolicy = async (explicitPath) => {
84
84
  const path = explicitPath ?? findNeonConfig();
85
- if (!path) return [];
85
+ if (!path) return void 0;
86
86
  const { config } = await loadConfigFromFile({ path });
87
- return declaredNeonServices(config);
87
+ return config;
88
88
  };
89
89
  const removeFileIfPresent = (path) => {
90
90
  try {
@@ -142,10 +142,13 @@ const builder = (argv) => argv.usage("$0 claim <sub-command> [options]").option(
142
142
  flag: "--service",
143
143
  onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
144
144
  }) : [];
145
- const configuredServices = await servicesFromConfig(typeof args.config === "string" ? args.config : void 0);
145
+ const policy = await loadCreatePolicy(typeof args.config === "string" ? args.config : void 0);
146
+ const configuredServices = policy ? declaredNeonServices(policy) : [];
147
+ const dataApi = policy ? claimableDataApiCreateBody(policy) : void 0;
146
148
  await create({
147
149
  ...args,
148
- services: [.../* @__PURE__ */ new Set([...services, ...configuredServices])]
150
+ services: [.../* @__PURE__ */ new Set([...services, ...configuredServices])],
151
+ ...dataApi ? { dataApi } : {}
149
152
  });
150
153
  }).command("status [project-id]", "Show a claimable project's lifecycle and claim status", (y) => y.positional("project-id", {
151
154
  describe: "Project from `neon claim list`. Defaults to the project linked in this directory",
@@ -208,7 +211,8 @@ const create = async (props) => {
208
211
  const client = new ClaimableClient(props.claimableHost);
209
212
  const registration = await client.register({
210
213
  capabilities: claimableCapabilities(props.services ?? []),
211
- source: "neon_cli"
214
+ source: "neon_cli",
215
+ ...props.dataApi ? { dataApi: props.dataApi } : {}
212
216
  });
213
217
  const stored = {
214
218
  version: 1,
@@ -75,12 +75,8 @@ const INSPECT_FIELDS = [
75
75
  "branch",
76
76
  "config"
77
77
  ];
78
- /**
79
- * Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
80
- * `process.env` before the policy is evaluated.
81
- */
82
78
  const envFlag = { env: {
83
- describe: "Path to a .env file to load into the environment before evaluating neon.ts (so function env values resolve from it). Existing env vars are not overridden.",
79
+ describe: "Path to a .env file to load into the environment before evaluating neon.ts so Function env values resolve from it. Existing env vars are not overridden. Function env values that read process.env must be set in this file or the environment.",
84
80
  type: "string"
85
81
  } };
86
82
  /** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
@@ -331,11 +327,13 @@ const liveConfigView = async (opts) => {
331
327
  view: toNeonConfigView(resolved, live.preview)
332
328
  };
333
329
  };
330
+ const loadEnvFileIfGiven = (env) => {
331
+ if (!env) return;
332
+ const applied = loadEnvFileIntoProcess(env);
333
+ log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, env, applied.join(", "));
334
+ };
334
335
  const loadConfig = async (props) => {
335
- if (props.env) {
336
- const applied = loadEnvFileIntoProcess(props.env);
337
- log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, props.env, applied.join(", "));
338
- }
336
+ loadEnvFileIfGiven(props.env);
339
337
  const { config } = await loadConfigFromFile$1({ ...props.config ? { path: props.config } : {} });
340
338
  return config;
341
339
  };
@@ -529,6 +527,7 @@ const assertAiGatewayProvisionableFromCreds = async (props) => {
529
527
  * see overridden. Functions are bundled with neonctl's own esbuild helper.
530
528
  */
531
529
  const applyPolicyOnCreate = async (props) => {
530
+ loadEnvFileIfGiven(props.env);
532
531
  let config;
533
532
  try {
534
533
  ({config} = await loadConfigFromFile$1({ ...props.cwd ? { cwd: props.cwd } : {} }));
@@ -588,6 +587,7 @@ const logPolicyResult = (result, opts) => {
588
587
  * (see the handler) instead of leaving it stranded behind an unchanged `.neon`.
589
588
  */
590
589
  const createBranchFromPolicyOnCheckout = async (props) => {
590
+ loadEnvFileIfGiven(props.env);
591
591
  let config;
592
592
  try {
593
593
  ({config} = await loadConfigFromFile$1({ ...props.cwd ? { cwd: props.cwd } : {} }));
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { fillSingleProject } from "../utils/enrichers.js";
3
3
  import { applyCmd, applyFlags, envFlag, envPullFlag } from "./config.js";
4
+ import { DEPLOY_COMMANDS_EPILOGUE } from "./deploy_help.js";
4
5
  //#region src/commands/deploy.ts
5
6
  var deploy_exports = /* @__PURE__ */ __exportAll({
6
7
  builder: () => builder,
@@ -26,7 +27,7 @@ const builder = (argv) => argv.usage("$0 deploy [options]").options({
26
27
  ...envFlag,
27
28
  ...applyFlags,
28
29
  ...envPullFlag
29
- }).middleware(fillSingleProject).strict();
30
+ }).middleware(fillSingleProject).epilogue(DEPLOY_COMMANDS_EPILOGUE).strict();
30
31
  const handler = (props) => applyCmd(props);
31
32
  //#endregion
32
33
  export { builder, command, describe, handler, deploy_exports as t };
@@ -0,0 +1,10 @@
1
+ //#region src/commands/deploy_help.ts
2
+ const DEPLOY_COMMANDS_EPILOGUE = [
3
+ "",
4
+ "Use neon deploy with a neon.ts file for a full deployment (declared services and functions).",
5
+ "Use neon functions deploy to deploy one function manually, including a targeted env update.",
6
+ "neon deploy --env <file> loads that .env file so neon.ts can read Function env from it.",
7
+ "neon functions deploy --env is KEY=VALUE (repeatable), not a file path."
8
+ ].join("\n");
9
+ //#endregion
10
+ export { DEPLOY_COMMANDS_EPILOGUE };