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.
- package/README.md +55 -13
- package/dist/_chunks/{credential_io-YeAxg9Xn.js → credential_io-D8AR-jvB.js} +4 -4
- package/dist/_chunks/{env-NbA61JR3.js → env-CZmstYrY.js} +71 -59
- package/dist/_chunks/{env_services-Tz9G4JeT.js → env_services-CAWrZTWa.js} +138 -41
- package/dist/_shared/auth_selection.js +83 -0
- package/dist/_shared/credentials.js +188 -0
- package/dist/_shared/paths.js +151 -0
- package/dist/_shared/profiles.js +226 -0
- package/dist/_shared/secure_file.js +41 -0
- package/dist/analytics.js +2 -2
- package/dist/auth.js +101 -11
- package/dist/auth_context.js +2 -11
- package/dist/claimable/api.js +2 -1
- package/dist/commands/ask.js +2 -1
- package/dist/commands/auth.js +155 -63
- package/dist/commands/checkout.js +28 -9
- package/dist/commands/claim.js +11 -7
- package/dist/commands/config.js +9 -9
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/deploy_help.js +10 -0
- package/dist/commands/dev.js +147 -36
- package/dist/commands/env.js +33 -44
- package/dist/commands/functions.js +63 -2
- package/dist/commands/profile.js +2 -2
- package/dist/config_services.js +36 -1
- package/dist/credential_io.js +1 -1
- package/dist/custom_domains_api.js +37 -0
- package/dist/dev/env.js +1 -1
- package/dist/dev/functions.js +28 -11
- package/dist/dev/websocket.js +1 -1
- package/dist/env_services.js +2 -2
- package/dist/index.js +73 -20
- package/dist/init/build_config.js +4 -0
- package/dist/neon_services.js +2 -2
- package/dist/psql/wire/connection.js +1 -1
- package/dist/refresh_lock.js +44 -0
- package/dist/retire_credential.js +1 -1
- package/dist/test_utils/neon_api_server.js +60 -0
- package/dist/test_utils/rotating_oauth_server.js +94 -0
- package/dist/utils/config_diff.js +2 -13
- package/package.json +7 -5
package/dist/commands/auth.js
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
129
|
-
*
|
|
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
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
169
|
+
if (isAccessTokenUsable(tokenSet, Date.now())) {
|
|
149
170
|
log.debug("Using existing valid access_token");
|
|
150
|
-
const
|
|
151
|
-
apiKey: tokenSet.access_token,
|
|
152
|
-
apiHost: props.apiHost
|
|
153
|
-
});
|
|
171
|
+
const apiKey = tokenSet.access_token ?? "";
|
|
154
172
|
return {
|
|
155
|
-
apiKey
|
|
156
|
-
apiClient
|
|
173
|
+
apiKey,
|
|
174
|
+
apiClient: getApiClient({
|
|
175
|
+
apiKey,
|
|
176
|
+
apiHost: props.apiHost
|
|
177
|
+
}),
|
|
178
|
+
refreshed: false
|
|
157
179
|
};
|
|
158
180
|
}
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 (
|
|
334
|
-
|
|
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
|
-
*
|
|
368
|
-
*
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 };
|
package/dist/commands/claim.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 } : {} }));
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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 };
|