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 +5 -4
- package/dist/claimable/state.js +20 -21
- package/dist/commands/auth.js +9 -11
- package/dist/commands/claim.js +20 -19
- package/dist/commands/dev.js +2 -1
- package/dist/commands/env.js +5 -1
- package/dist/context.js +7 -1
- package/dist/env_file.js +2 -2
- package/package.json +5 -5
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
|
|
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
|
|
127
|
-
token and send API calls to Claimable Neon. The service decides which
|
|
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
|
package/dist/claimable/state.js
CHANGED
|
@@ -77,35 +77,34 @@ const removeClaimableCredentials = (configDir, projectId) => {
|
|
|
77
77
|
throw error;
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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() === "") &&
|
|
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`
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
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,
|
|
110
|
+
export { assertionHasExpired, claimableApiHost, claimableCredentialsExist, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, shouldUseClaimableCredentials, writeClaimableCredentials };
|
package/dist/commands/auth.js
CHANGED
|
@@ -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,
|
|
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
|
|
321
|
-
if (
|
|
322
|
-
const
|
|
323
|
-
|
|
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.
|
|
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
|
package/dist/commands/claim.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
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,
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
};
|
package/dist/commands/env.js
CHANGED
|
@@ -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(
|
|
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
|
|
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.
|
|
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/
|
|
100
|
+
"@neon-internals/cli-core": "0.0.0",
|
|
101
101
|
"@neon/e2e-harness": "0.0.0",
|
|
102
|
-
"@neon/
|
|
103
|
-
"@neon
|
|
102
|
+
"@neon-internals/env-core": "0.0.10",
|
|
103
|
+
"@neon/functions": "0.9.0"
|
|
104
104
|
},
|
|
105
105
|
"publishConfig": {
|
|
106
106
|
"access": "public",
|