neon 4.14.4 → 4.14.6

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 CHANGED
@@ -116,22 +116,23 @@ to skip the dotenv write.
116
116
 
117
117
  The command writes:
118
118
 
119
- - a `.neon` context that identifies the project and Claimable Neon service;
119
+ - a `.neon` context with the project id and pinned branch;
120
120
  - an owner-only identity assertion under the CLI config directory;
121
121
  - `DATABASE_URL`, `DATABASE_URL_UNPOOLED`, `NEON_BRANCH`, and Auth / Data API variables
122
122
  when `neon.ts` declares them (or, with no `neon.ts`, when they are provisioned) to `.env`
123
123
  or `.env.local` (disable this with `--no-env-pull`). The write is the same `env pull` used
124
124
  after create.
125
125
 
126
- Subsequent project commands automatically exchange the assertion for a short-lived agent
127
- token and send API calls to Claimable Neon. The service decides which operations are
128
- allowed before claim.
126
+ Subsequent project commands find that assertion by the linked project id, exchange it for a
127
+ short-lived agent token, and send API calls to Claimable Neon. The service decides which
128
+ operations are allowed before claim.
129
129
 
130
130
  ```bash
131
131
  neon claim status # lifecycle and transfer status
132
132
  neon projects get <project-id> # regular CLI command, same agent token
133
133
  neon psql --role-name neondb_owner -- -c "select now()"
134
134
  neon config plan
135
+ neon checkout main
135
136
  neon env pull
136
137
 
137
138
  neon claim accept # create a claim code and open the transfer URL
@@ -77,35 +77,34 @@ const removeClaimableCredentials = (configDir, projectId) => {
77
77
  throw error;
78
78
  }
79
79
  };
80
- const resolveClaimableContext = (context) => {
81
- const marker = context.claimable;
82
- if (marker === void 0) return null;
83
- if (!isRecord(marker)) throw new Error("The linked .neon file has an invalid \"claimable\" marker. Delete the claimable field from .neon, or delete .neon.");
84
- if (marker.version !== 1) throw new Error(`Unsupported Claimable Neon context version in .neon. Update the Neon CLI before using this project.`);
85
- if (!nonEmptyString(marker.origin) || !nonEmptyString(context.projectId)) throw new Error("The linked .neon file has an incomplete \"claimable\" marker. Delete the claimable field from .neon, or delete .neon.");
86
- assertProjectId(context.projectId);
87
- return {
88
- origin: marker.origin,
89
- projectId: context.projectId,
90
- ...nonEmptyString(context.branch) ? { branch: context.branch } : {}
91
- };
80
+ const readLinkedClaimableCredentials = (configDir, context) => {
81
+ if (!nonEmptyString(context.projectId)) return null;
82
+ if (!PROJECT_ID.test(context.projectId)) return null;
83
+ return readClaimableCredentials(configDir, context.projectId);
84
+ };
85
+ const claimableCredentialsExist = (configDir, projectId) => {
86
+ if (!PROJECT_ID.test(projectId)) return false;
87
+ return existsSync(claimableCredentialsPath(configDir, projectId));
92
88
  };
93
- const shouldUseClaimableCredentials = (inputs, profileFlag, context) => inputs.apiKeyFlag.trim() === "" && inputs.apiKeyEnv.trim() === "" && inputs.profileEnv.trim() === "" && (profileFlag === void 0 || profileFlag.trim() === "") && resolveClaimableContext(context) !== null;
89
+ const shouldUseClaimableCredentials = (inputs, profileFlag, context, configDir) => inputs.apiKeyFlag.trim() === "" && inputs.apiKeyEnv.trim() === "" && inputs.profileEnv.trim() === "" && (profileFlag === void 0 || profileFlag.trim() === "") && readLinkedClaimableCredentials(configDir, context) !== null;
94
90
  /** Management API base URL the CLI uses for a Claimable Neon origin. */
95
91
  const claimableApiHost = (origin) => `${new ClaimableClient(origin).origin}/v1`;
96
92
  /**
97
93
  * Whether this invocation is talking to Claimable Neon, so env resolve must not
98
94
  * mint credentials or pull services that require claim.
99
95
  *
100
- * Auth source wins so a same-process checkout that rewrote `.neon` without the
101
- * marker still skips minting. A `.neon` marker plus a production API host is an
102
- * account override and is not claimable.
96
+ * Auth source wins so a same-process checkout that rewrote `.neon` still skips
97
+ * minting. A credential file plus a production API host is an account override
98
+ * and is not claimable.
103
99
  */
104
100
  const isClaimableEnvTarget = (props) => {
105
- if (getAuthContext()?.source === "claimable") return true;
106
- const linked = resolveClaimableContext(readContextFile(props.contextFile));
107
- if (linked === null) return false;
108
- return props.apiHost.trim().replace(/\/+$/, "") === claimableApiHost(linked.origin);
101
+ const source = getAuthContext()?.source;
102
+ if (source === "claimable") return true;
103
+ if (source !== void 0) return false;
104
+ if (props.configDir.trim() === "") return false;
105
+ const stored = readLinkedClaimableCredentials(props.configDir, readContextFile(props.contextFile));
106
+ if (stored === null) return false;
107
+ return props.apiHost.trim().replace(/\/+$/, "") === claimableApiHost(stored.origin);
109
108
  };
110
109
  //#endregion
111
- export { assertionHasExpired, claimableApiHost, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials, writeClaimableCredentials };
110
+ export { assertionHasExpired, claimableApiHost, claimableCredentialsExist, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, shouldUseClaimableCredentials, writeClaimableCredentials };
@@ -5,13 +5,13 @@ import { log } from "../log.js";
5
5
  import { getApiClient, isNeonApiError } from "../api.js";
6
6
  import { setAuthContext } from "../auth_context.js";
7
7
  import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
8
- import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
8
+ import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, 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
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
- import { assertionHasExpired, claimableCredentialsPath, readClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials } from "../claimable/state.js";
14
+ import { assertionHasExpired, claimableCredentialsExist, claimableCredentialsPath, readLinkedClaimableCredentials, shouldUseClaimableCredentials } from "../claimable/state.js";
15
15
  import { withExclusiveLock } from "../refresh_lock.js";
16
16
  import { readOutgoingCredential, retirePreviousCredential } from "../retire_credential.js";
17
17
  import { existsSync } from "node:fs";
@@ -291,6 +291,7 @@ const ensureAuth = async (props) => {
291
291
  if (isConfigInit(props)) return;
292
292
  if (isProfileCommand(props)) return;
293
293
  if (isMcpOauth(props)) return;
294
+ if (isInspectDbUrl(props)) return;
294
295
  if (props._[0] === "open") return;
295
296
  if (isSkillsCommand(props) || isPluginsCommand(props)) return;
296
297
  if (isAskCommand(props)) return;
@@ -316,15 +317,12 @@ const ensureAuth = async (props) => {
316
317
  const inputs = credentialInputs();
317
318
  const contextFile = typeof props.contextFile === "function" ? props.contextFile() : props.contextFile ?? currentContextFile();
318
319
  const localContext = readContextFile(contextFile);
319
- if (shouldUseClaimableCredentials(inputs, props.profile, localContext)) {
320
- const linked = resolveClaimableContext(localContext);
321
- if (linked === null) throw new Error("The linked Claimable Neon context could not be resolved.");
322
- const stored = readClaimableCredentials(props.configDir, linked.projectId);
323
- const path = claimableCredentialsPath(props.configDir, linked.projectId);
324
- if (stored === null) throw new Error(`The linked project is claimable, but its identity assertion is missing from ${path}. Run \`neon claim create\` in a new directory, or \`neon link\` after claiming the project.`);
325
- if (assertionHasExpired(stored)) throw new Error(`The identity assertion for ${linked.projectId} has expired. Run \`neon claim delete ${linked.projectId} --yes\` to drop the local record.`);
320
+ if (shouldUseClaimableCredentials(inputs, props.profile, localContext, props.configDir)) {
321
+ const stored = readLinkedClaimableCredentials(props.configDir, localContext);
322
+ if (stored === null || localContext.projectId === void 0) throw new Error("The linked Claimable Neon identity assertion could not be read.");
323
+ const path = claimableCredentialsPath(props.configDir, localContext.projectId);
324
+ if (assertionHasExpired(stored)) throw new Error(`The identity assertion for ${localContext.projectId} has expired. Run \`neon claim delete ${localContext.projectId} --yes\` to drop the local record.`);
326
325
  const client = new ClaimableClient(stored.origin);
327
- if (client.origin !== new ClaimableClient(linked.origin).origin) throw new Error(`The linked .neon file and ${path} name different Claimable Neon services. Delete .neon or the assertion file and run \`neon claim create\` in a new directory.`);
328
326
  let token;
329
327
  try {
330
328
  token = await client.exchange(stored.identityAssertion);
@@ -346,7 +344,7 @@ const ensureAuth = async (props) => {
346
344
  log.debug("Using the linked Claimable Neon project's short-lived access token");
347
345
  return;
348
346
  }
349
- if (localContext.claimable !== void 0) log.warning("This directory is linked to a claimable project, but NEON_API_KEY or NEON_PROFILE is set. This command will use that account credential instead of the unclaimed project. Unset them to keep using the unclaimed project.");
347
+ if (localContext.projectId !== void 0 && claimableCredentialsExist(props.configDir, localContext.projectId)) log.warning("This directory is linked to a claimable project, but NEON_API_KEY or NEON_PROFILE is set. This command will use that account credential instead of the unclaimed project. Unset them to keep using the unclaimed project.");
350
348
  const selection = selectCredential({
351
349
  ...inputs,
352
350
  profileFlag: props.profile
@@ -8,7 +8,7 @@ import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagVal
8
8
  import { resolveEnvFilePath } from "../env_file.js";
9
9
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
10
10
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
11
- import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, writeClaimableCredentials } from "../claimable/state.js";
11
+ import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, writeClaimableCredentials } from "../claimable/state.js";
12
12
  import { writer } from "../writer.js";
13
13
  import { noPassthrough } from "../utils/flags.js";
14
14
  import { pull } from "./env.js";
@@ -184,19 +184,23 @@ const resolveTarget = (props) => {
184
184
  rejectExplicitAccountCredential(props);
185
185
  const requested = props.projectId?.trim();
186
186
  const context = readContextFile(props.contextFile);
187
- const linked = resolveClaimableContext(context);
188
- const projectId = requested || linked?.projectId;
189
- if (projectId === void 0) throw new Error("This directory is not linked to a claimable project. Pass a project id from `neon claim list`, or run `neon claim create` first.");
190
- const credentials = readClaimableCredentials(props.configDir, projectId);
191
- if (credentials === null) throw new Error(`The identity assertion for ${projectId} is missing. The project cannot be managed from this machine; claim it through its existing verification URL or run \`neon link\` after it is claimed.`);
192
- const client = new ClaimableClient(credentials.origin);
193
- const contextMatches = linked?.projectId === projectId;
194
- if (contextMatches && linked !== null && client.origin !== new ClaimableClient(linked.origin).origin) throw new Error("The .neon context and saved identity assertion name different Claimable Neon services. Delete .neon or the assertion file and run `neon claim create` in a new directory.");
187
+ if (requested) {
188
+ const credentials = readClaimableCredentials(props.configDir, requested);
189
+ if (credentials === null) throw new Error(`The identity assertion for ${requested} is missing. The project cannot be managed from this machine; claim it through its existing verification URL or run \`neon link\` after it is claimed.`);
190
+ return {
191
+ projectId: requested,
192
+ credentials,
193
+ client: new ClaimableClient(credentials.origin),
194
+ contextMatches: context.projectId === requested
195
+ };
196
+ }
197
+ const linked = readLinkedClaimableCredentials(props.configDir, context);
198
+ if (linked === null) throw new Error("This directory is not linked to a claimable project. Pass a project id from `neon claim list`, or run `neon claim create` first.");
195
199
  return {
196
- projectId,
197
- credentials,
198
- client,
199
- contextMatches
200
+ projectId: linked.projectId,
201
+ credentials: linked,
202
+ client: new ClaimableClient(linked.origin),
203
+ contextMatches: true
200
204
  };
201
205
  };
202
206
  const requireLiveIdentity = (credentials) => {
@@ -210,7 +214,7 @@ const clearLocalRecord = (props, projectId, contextMatches) => {
210
214
  const create = async (props) => {
211
215
  rejectExplicitAccountCredential(props);
212
216
  const existing = readContextFile(props.contextFile);
213
- if (existing.projectId || existing.orgId || existing.claimable) throw new Error(`${props.contextFile} already links this directory to a Neon project. Run \`neon claim create\` from an unlinked directory.`);
217
+ if (existing.projectId || existing.orgId) throw new Error(`${props.contextFile} already links this directory to a Neon project. Run \`neon claim create\` from an unlinked directory.`);
214
218
  const contextFileExisted = existsSync(props.contextFile);
215
219
  const cwd = props.cwd ?? process.cwd();
216
220
  const envFile = props.envPull ? resolveEnvFilePath(cwd, props.file) : void 0;
@@ -241,11 +245,7 @@ const create = async (props) => {
241
245
  localStateWritten = true;
242
246
  applyContext(props.contextFile, {
243
247
  projectId: registration.project.id,
244
- branch: registration.project.branchId,
245
- claimable: {
246
- version: 1,
247
- origin: client.origin
248
- }
248
+ branch: registration.project.branchId
249
249
  });
250
250
  contextWritten = true;
251
251
  const token = await client.exchange(registration.identityAssertion);
@@ -261,6 +261,7 @@ const create = async (props) => {
261
261
  apiClient,
262
262
  apiKey: token.accessToken,
263
263
  apiHost,
264
+ configDir: props.configDir,
264
265
  contextFile: props.contextFile,
265
266
  output: props.output,
266
267
  projectId: registration.project.id,
@@ -89,7 +89,8 @@ const devEnvContext = (props, branchId, cwd) => {
89
89
  ...props.apiHost ? { apiHost: props.apiHost } : {},
90
90
  ...isClaimableEnvTarget({
91
91
  apiHost: props.apiHost,
92
- contextFile: props.contextFile ?? ""
92
+ contextFile: props.contextFile ?? "",
93
+ configDir: props.configDir ?? ""
93
94
  }) ? { claimable: true } : {}
94
95
  };
95
96
  };
@@ -122,7 +122,11 @@ const NEON_OWNED_ENV_KEYS = [
122
122
  ];
123
123
  const pull = async (props, opts = {}) => {
124
124
  const cwd = props.cwd ?? process.cwd();
125
- const claimable = isClaimableEnvTarget(props);
125
+ const claimable = isClaimableEnvTarget({
126
+ apiHost: props.apiHost,
127
+ contextFile: props.contextFile,
128
+ configDir: props.configDir ?? ""
129
+ });
126
130
  const dropped = claimable && (props.services !== void 0 || props.envKeys !== void 0) ? dropClaimableUnsupported(props.services ?? [], props.envKeys ?? []) : null;
127
131
  const selectionServices = dropped ? dropped.services : props.services ?? [];
128
132
  const selectionEnvKeys = dropped ? dropped.envKeys : props.envKeys ?? [];
package/dist/context.js CHANGED
@@ -55,6 +55,12 @@ const isMcpCommand = (args) => args._[0] === "mcp";
55
55
  const isSkillsCommand = (args) => args._[0] === "skills" || args._[0] === "skill";
56
56
  const isPluginsCommand = (args) => args._[0] === "plugins" || args._[0] === "plugin";
57
57
  const isAskCommand = (args) => args._[0] === "ask";
58
+ /**
59
+ * `inspect db --db-url` talks to Postgres with that URI. Skip only when the
60
+ * parsed value is nonempty: empty `--db-url` still goes through project
61
+ * resolution and needs an API client.
62
+ */
63
+ const isInspectDbUrl = (args) => (args._[0] === "inspect" || args._[0] === "inspection") && typeof args.dbUrl === "string" && args.dbUrl.length > 0;
58
64
  /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
59
65
  const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
60
66
  const OAUTH_FALSE = /* @__PURE__ */ new Set([
@@ -242,4 +248,4 @@ const globToRegExp = (pattern) => {
242
248
  return new RegExp(`^${source}$`);
243
249
  };
244
250
  //#endregion
245
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
251
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isInspectDbUrl, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
package/dist/env_file.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  //#region src/env_file.ts
4
4
  /**
5
5
  * Default dotenv file `env pull` writes to: `.env` when one already exists in the working
@@ -7,7 +7,7 @@ import { join } from "node:path";
7
7
  * `vercel env pull` convention. An explicit `--file` always wins over this.
8
8
  */
9
9
  const resolveEnvFilePath = (cwd, file) => {
10
- if (file) return join(cwd, file);
10
+ if (file) return resolve(cwd, file);
11
11
  if (existsSync(join(cwd, ".env"))) return join(cwd, ".env");
12
12
  return join(cwd, ".env.local");
13
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.14.4",
3
+ "version": "4.14.6",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,8 +63,8 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.3.2",
67
66
  "@neon/config-runtime": "1.2.3",
67
+ "@neon/config": "1.3.2",
68
68
  "@neon/sdk": "4.0.0"
69
69
  },
70
70
  "optionalDependencies": {
@@ -97,10 +97,10 @@
97
97
  "tsx": "4.22.3",
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
- "@neon-internals/env-core": "0.0.10",
100
+ "@neon-internals/cli-core": "0.0.0",
101
101
  "@neon/e2e-harness": "0.0.0",
102
- "@neon/functions": "0.9.0",
103
- "@neon-internals/cli-core": "0.0.0"
102
+ "@neon-internals/env-core": "0.0.10",
103
+ "@neon/functions": "0.9.0"
104
104
  },
105
105
  "publishConfig": {
106
106
  "access": "public",