neon 2.38.0 → 2.38.2
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/dist/auth_context.js +16 -0
- package/dist/commands/auth.js +13 -0
- package/dist/commands/env.js +24 -4
- package/dist/dev/env.js +9 -11
- package/dist/index.js +10 -1
- package/package.json +6 -4
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let current = null;
|
|
2
|
+
export const setAuthContext = (context) => {
|
|
3
|
+
current = context;
|
|
4
|
+
};
|
|
5
|
+
export const getAuthContext = () => current;
|
|
6
|
+
/**
|
|
7
|
+
* The config directory whose credentials a 401 should clear, or `null` to leave
|
|
8
|
+
* stored credentials alone.
|
|
9
|
+
*
|
|
10
|
+
* An API key passed on the command line never touches the stored OAuth token,
|
|
11
|
+
* so a 401 on that key says nothing about whether the stored credentials are
|
|
12
|
+
* still good — clearing them would sign the user out of an account the failed
|
|
13
|
+
* request never used. The directory comes from the context rather than the
|
|
14
|
+
* default so that `--config-dir` isolates the deletion too.
|
|
15
|
+
*/
|
|
16
|
+
export const credentialsToClearOn401 = (context) => context?.source === "stored-credentials" ? context.configDir : null;
|
package/dist/commands/auth.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getApiClient } from "../api.js";
|
|
5
5
|
import { auth, refreshToken } from "../auth.js";
|
|
6
|
+
import { setAuthContext } from "../auth_context.js";
|
|
6
7
|
import { CREDENTIALS_FILE } from "../config.js";
|
|
7
8
|
import { isConfigInit, isCurrentBranchProbe } from "../context.js";
|
|
8
9
|
import { isCi } from "../env.js";
|
|
@@ -129,6 +130,10 @@ export const ensureAuth = async (props) => {
|
|
|
129
130
|
if (props.apiKey || props._[0] === "auth") {
|
|
130
131
|
if (props.apiKey) {
|
|
131
132
|
log.debug("Using an API key to authorize requests");
|
|
133
|
+
setAuthContext({
|
|
134
|
+
source: "api-key",
|
|
135
|
+
configDir: props.configDir,
|
|
136
|
+
});
|
|
132
137
|
}
|
|
133
138
|
props.apiClient = getApiClient({
|
|
134
139
|
apiKey: props.apiKey,
|
|
@@ -149,6 +154,10 @@ export const ensureAuth = async (props) => {
|
|
|
149
154
|
if (result) {
|
|
150
155
|
props.apiKey = result.apiKey;
|
|
151
156
|
props.apiClient = result.apiClient;
|
|
157
|
+
setAuthContext({
|
|
158
|
+
source: "stored-credentials",
|
|
159
|
+
configDir: props.configDir,
|
|
160
|
+
});
|
|
152
161
|
return;
|
|
153
162
|
}
|
|
154
163
|
}
|
|
@@ -189,6 +198,10 @@ export const ensureAuth = async (props) => {
|
|
|
189
198
|
apiKey,
|
|
190
199
|
apiHost: props.apiHost,
|
|
191
200
|
});
|
|
201
|
+
setAuthContext({
|
|
202
|
+
source: "stored-credentials",
|
|
203
|
+
configDir: props.configDir,
|
|
204
|
+
});
|
|
192
205
|
};
|
|
193
206
|
/**
|
|
194
207
|
* Deletes the credentials file at the specified path
|
package/dist/commands/env.js
CHANGED
|
@@ -82,7 +82,7 @@ export const pull = async (props, opts = {}) => {
|
|
|
82
82
|
// Reuse `neon dev`'s tiered resolver (neon.ts policy -> plan gate -> fetchEnv, else
|
|
83
83
|
// pullConfig -> fetchEnv). Unlike dev, an unresolved context or failure is surfaced —
|
|
84
84
|
// `env pull` is an explicit action, so it should error rather than write nothing.
|
|
85
|
-
const vars = await resolveNeonEnvVars({
|
|
85
|
+
const { vars, credential } = await resolveNeonEnvVars({
|
|
86
86
|
cwd,
|
|
87
87
|
projectId: props.projectId,
|
|
88
88
|
branchId,
|
|
@@ -107,6 +107,15 @@ export const pull = async (props, opts = {}) => {
|
|
|
107
107
|
if (removed.length > 0) {
|
|
108
108
|
log.info("Removed %d stale Neon variable%s not enabled on this branch: %s", removed.length, removed.length === 1 ? "" : "s", removed.join(", "));
|
|
109
109
|
}
|
|
110
|
+
// A new credential means the values that back object storage / the AI Gateway just
|
|
111
|
+
// changed, so anything else holding the old ones (a deployed preview, a second checkout)
|
|
112
|
+
// needs the new values. Name the keys rather than leaving the user to diff the file.
|
|
113
|
+
if (credential?.issued) {
|
|
114
|
+
log.info("Issued a new branch credential — these now hold fresh values: %s", credential.keys.join(", "));
|
|
115
|
+
if (credential.revoked.length > 0) {
|
|
116
|
+
log.info("Revoked the credential it replaced (%s).", credential.revoked.join(", "));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
110
119
|
// A dotenv file *we* create holds live branch credentials (DATABASE_URL, Auth keys, service
|
|
111
120
|
// tokens), so ignore it the same way the `.neon` context file is — otherwise a fresh repo is
|
|
112
121
|
// one `git add -A` away from committing them. Only on creation: re-adding the entry on every
|
|
@@ -128,7 +137,12 @@ export const pull = async (props, opts = {}) => {
|
|
|
128
137
|
gateway: { baseUrl: gatewayBaseUrl, token: gatewayToken },
|
|
129
138
|
});
|
|
130
139
|
}
|
|
131
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
status: "written",
|
|
142
|
+
written,
|
|
143
|
+
file: targetPath,
|
|
144
|
+
...(credential && credential.keys.length > 0 ? { credential } : {}),
|
|
145
|
+
};
|
|
132
146
|
};
|
|
133
147
|
/**
|
|
134
148
|
* Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
|
|
@@ -162,8 +176,14 @@ export const autoPullEnvAfterPin = async (props) => {
|
|
|
162
176
|
*/
|
|
163
177
|
export const renderAgentPullNote = (result) => {
|
|
164
178
|
switch (result.status) {
|
|
165
|
-
case "written":
|
|
166
|
-
|
|
179
|
+
case "written": {
|
|
180
|
+
// Call out a re-issued credential: an agent that already wrote the old storage /
|
|
181
|
+
// gateway values somewhere else has to update them.
|
|
182
|
+
const credential = result.credential?.issued
|
|
183
|
+
? ` Issued a new branch credential, so ${result.credential.keys.join(", ")} changed.`
|
|
184
|
+
: "";
|
|
185
|
+
return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.${credential}`;
|
|
186
|
+
}
|
|
167
187
|
case "empty":
|
|
168
188
|
return " No Neon env vars to pull for this branch yet.";
|
|
169
189
|
case "skipped":
|
package/dist/dev/env.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfigFromFile } from "@neon/config";
|
|
2
2
|
import { plan, pullConfig } from "@neon/config-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { fetchEnvReusingSecrets, } from "@neon/env/runtime";
|
|
4
4
|
import { log } from "../log.js";
|
|
5
5
|
/** The API-targeting options every runtime call forwards from the context. */
|
|
6
6
|
const apiOptions = (ctx) => ({
|
|
@@ -102,7 +102,8 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
102
102
|
*/
|
|
103
103
|
export const resolveDevEnv = async (ctx) => {
|
|
104
104
|
try {
|
|
105
|
-
|
|
105
|
+
const { vars, credential } = await resolveNeonEnvVars(ctx);
|
|
106
|
+
return { vars, credential };
|
|
106
107
|
}
|
|
107
108
|
catch (err) {
|
|
108
109
|
if (err instanceof DevEnvMismatchError)
|
|
@@ -178,15 +179,12 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
|
178
179
|
const isMissingResource = (change) => change.kind === "service" &&
|
|
179
180
|
change.action === "create" &&
|
|
180
181
|
!change.identifier.startsWith("function:");
|
|
181
|
-
const fetchAndProject = async (config, ctx) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
return toEntries(env);
|
|
189
|
-
};
|
|
182
|
+
const fetchAndProject = async (config, ctx) => fetchEnvReusingSecrets(config, {
|
|
183
|
+
projectId: ctx.projectId,
|
|
184
|
+
branch: ctx.branchId,
|
|
185
|
+
...apiOptions(ctx),
|
|
186
|
+
...(ctx.env ? { env: ctx.env } : {}),
|
|
187
|
+
});
|
|
190
188
|
/**
|
|
191
189
|
* Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
|
|
192
190
|
* root. Returns `null` when there is none (the common "no config" case), and
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
4
4
|
import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent, } from "./analytics.js";
|
|
5
5
|
import { isNeonApiError, messageFromBody } from "./api.js";
|
|
6
6
|
import { defaultClientID } from "./auth.js";
|
|
7
|
+
import { credentialsToClearOn401, getAuthContext } from "./auth_context.js";
|
|
7
8
|
import { deleteCredentials, ensureAuth } from "./commands/auth.js";
|
|
8
9
|
import commands from "./commands/index.js";
|
|
9
10
|
import { defaultDir, ensureConfigDir } from "./config.js";
|
|
@@ -175,9 +176,17 @@ async function handleError(msg, err) {
|
|
|
175
176
|
}
|
|
176
177
|
else if (err.status === 401) {
|
|
177
178
|
sendError(err, "AUTH_FAILED");
|
|
179
|
+
const configDir = credentialsToClearOn401(getAuthContext());
|
|
180
|
+
// The request was authorized with a key the user supplied, so there
|
|
181
|
+
// is nothing of ours to clear and nothing to retry — the same key
|
|
182
|
+
// would just be rejected again.
|
|
183
|
+
if (configDir === null) {
|
|
184
|
+
log.error("Authentication failed: the Neon API rejected the API key. Check --api-key or NEON_API_KEY.");
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
178
187
|
log.info("Authentication failed, deleting credentials...");
|
|
179
188
|
try {
|
|
180
|
-
deleteCredentials(
|
|
189
|
+
deleteCredentials(configDir);
|
|
181
190
|
return true; // Allow retry for auth failures
|
|
182
191
|
}
|
|
183
192
|
catch (deleteErr) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "2.38.
|
|
3
|
+
"version": "2.38.2",
|
|
4
4
|
"description": "CLI tool for Neon Serverless Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"cliui": "8.0.1",
|
|
48
48
|
"diff": "5.2.0",
|
|
49
49
|
"fflate": "^0.8.3",
|
|
50
|
-
"neon-init": "0.20.
|
|
50
|
+
"neon-init": "0.20.3",
|
|
51
51
|
"open": "^10.2.0",
|
|
52
52
|
"openid-client": "6.8.1",
|
|
53
53
|
"pg-protocol": "^1.14.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@neon/sdk": "1.3.0",
|
|
60
60
|
"@neon/config": "0.10.0",
|
|
61
61
|
"@neon/config-runtime": "0.10.0",
|
|
62
|
-
"@neon/env": "0.
|
|
62
|
+
"@neon/env": "0.12.0"
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
65
|
"esbuild": "0.28.1"
|
|
@@ -88,7 +88,8 @@
|
|
|
88
88
|
"strip-ansi": "7.1.0",
|
|
89
89
|
"tsx": "4.22.3",
|
|
90
90
|
"typescript": "^5.9.0",
|
|
91
|
-
"vitest": "^3.0.9"
|
|
91
|
+
"vitest": "^3.0.9",
|
|
92
|
+
"@neon/e2e-harness": "0.0.0"
|
|
92
93
|
},
|
|
93
94
|
"publishConfig": {
|
|
94
95
|
"access": "public",
|
|
@@ -118,6 +119,7 @@
|
|
|
118
119
|
"lint:fix": "pnpm typecheck && biome check src --write",
|
|
119
120
|
"test": "pnpm --filter neonctl... build && vitest run",
|
|
120
121
|
"test:ci": "pnpm build && vitest run",
|
|
122
|
+
"test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
|
|
121
123
|
"test:conformance": "vitest run --config tests/psql-conformance/vitest.config.ts"
|
|
122
124
|
}
|
|
123
125
|
}
|