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/dev.js
CHANGED
|
@@ -2,10 +2,11 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { getCliName } from "../utils/cli_name.js";
|
|
4
4
|
import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
5
|
+
import { _ as functionBaseUrlKey } from "../_chunks/env_services-CAWrZTWa.js";
|
|
5
6
|
import { branchIdResolve } from "../utils/enrichers.js";
|
|
6
7
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
7
|
-
import { i as resolveDevEnv } from "../_chunks/env-
|
|
8
|
-
import { resolveFunctionsFromConfig } from "../dev/functions.js";
|
|
8
|
+
import { i as resolveDevEnv } from "../_chunks/env-CZmstYrY.js";
|
|
9
|
+
import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
|
|
9
10
|
import { resolveWatchInputs } from "../dev/inputs.js";
|
|
10
11
|
import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
12
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -14,6 +15,7 @@ import chalk from "chalk";
|
|
|
14
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
15
16
|
import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
|
|
16
17
|
import { once } from "node:events";
|
|
18
|
+
import { createServer } from "node:net";
|
|
17
19
|
//#region src/commands/dev.ts
|
|
18
20
|
var dev_exports = /* @__PURE__ */ __exportAll({
|
|
19
21
|
builder: () => builder,
|
|
@@ -24,7 +26,10 @@ var dev_exports = /* @__PURE__ */ __exportAll({
|
|
|
24
26
|
diffUnits: () => diffUnits,
|
|
25
27
|
formatEnvSummary: () => formatEnvSummary,
|
|
26
28
|
handler: () => handler,
|
|
27
|
-
|
|
29
|
+
localFunctionUrlEnv: () => localFunctionUrlEnv,
|
|
30
|
+
planFunctionsToUnits: () => planFunctionsToUnits,
|
|
31
|
+
reportDevCredential: () => reportDevCredential,
|
|
32
|
+
sourceFunctionUrlOverlay: () => sourceFunctionUrlOverlay
|
|
28
33
|
});
|
|
29
34
|
const command = "dev";
|
|
30
35
|
const describe = "Run Neon Functions locally with a dev server";
|
|
@@ -44,6 +49,11 @@ const builder = (argv) => argv.usage("$0 dev [--source <path>] [options]").examp
|
|
|
44
49
|
"object storage and the AI Gateway where the branch has them. A neon.ts in",
|
|
45
50
|
"this directory decides instead, exactly as it does for `env pull`.",
|
|
46
51
|
"",
|
|
52
|
+
"NEON_FUNCTION_<SLUG>_BASE_URL from `env pull` / `neon-env run` is the production",
|
|
53
|
+
"invocation URL. `dev` rewrites it to `http://localhost:<port>` for each function",
|
|
54
|
+
"this process is serving (every neon.ts function, or the matching slug when",
|
|
55
|
+
"`--source` names one).",
|
|
56
|
+
"",
|
|
47
57
|
"`dev` reads your .env / .env.local to reuse the branch credential behind the",
|
|
48
58
|
"AI Gateway and object storage, and never writes to them. With no such file it",
|
|
49
59
|
"issues a credential on every start, so run `env pull` once if you restart often."
|
|
@@ -110,11 +120,16 @@ const runSingleSource = async (props) => {
|
|
|
110
120
|
const branchId = await resolveBranchId(props);
|
|
111
121
|
const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
|
|
112
122
|
reportDevCredential(credential);
|
|
123
|
+
const match = await findConfigFunctionBySource(process.cwd(), source);
|
|
124
|
+
const { overlay, port } = await sourceFunctionUrlOverlay(match, props.port);
|
|
113
125
|
const unit = {
|
|
114
126
|
slug: null,
|
|
115
127
|
source,
|
|
116
128
|
bundleDir: devBundleDir(process.cwd()),
|
|
117
|
-
childEnv: buildChildEnv(
|
|
129
|
+
childEnv: buildChildEnv({
|
|
130
|
+
...neonEnv,
|
|
131
|
+
...overlay
|
|
132
|
+
}, port),
|
|
118
133
|
label: null,
|
|
119
134
|
envSummary: {
|
|
120
135
|
neon: Object.keys(neonEnv),
|
|
@@ -135,11 +150,11 @@ const runFromConfig = async (props) => {
|
|
|
135
150
|
if (functions.length === 0) throw new Error("neon.ts has no functions to serve. Add at least one under `preview.functions`, or pass --source <path>.");
|
|
136
151
|
const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
|
|
137
152
|
reportDevCredential(credential);
|
|
138
|
-
const units = planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
|
|
139
|
-
const replan = async (searchBase) => {
|
|
153
|
+
const units = await planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
|
|
154
|
+
const replan = async (searchBase, keepPorts) => {
|
|
140
155
|
const re = await resolveFunctionsFromConfig(process.cwd());
|
|
141
156
|
if (re === null) return null;
|
|
142
|
-
return planFunctionsToUnits(re.functions, neonEnv, searchBase);
|
|
157
|
+
return planFunctionsToUnits(re.functions, neonEnv, searchBase, keepPorts);
|
|
143
158
|
};
|
|
144
159
|
await runSupervisor(units, {
|
|
145
160
|
reload: {
|
|
@@ -149,22 +164,112 @@ const runFromConfig = async (props) => {
|
|
|
149
164
|
...skipped ? { envNote: skipped.reason } : {}
|
|
150
165
|
});
|
|
151
166
|
};
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
/** Ports are chosen before spawn so every sibling's localhost URL can be overlaid. */
|
|
168
|
+
const planFunctionsToUnits = async (functions, neonEnv, searchBase, keepPorts = /* @__PURE__ */ new Map()) => {
|
|
169
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
170
|
+
const portBySlug = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const fn of functions) {
|
|
172
|
+
if (fn.port === void 0) continue;
|
|
173
|
+
portBySlug.set(fn.slug, fn.port);
|
|
174
|
+
reserved.add(fn.port);
|
|
175
|
+
}
|
|
176
|
+
for (const fn of functions) {
|
|
177
|
+
if (portBySlug.has(fn.slug)) continue;
|
|
178
|
+
const kept = keepPorts.get(fn.slug);
|
|
179
|
+
if (kept !== void 0 && !reserved.has(kept)) {
|
|
180
|
+
portBySlug.set(fn.slug, kept);
|
|
181
|
+
reserved.add(kept);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
161
184
|
let searchOffset = 0;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
for (const fn of functions) {
|
|
186
|
+
if (portBySlug.has(fn.slug)) continue;
|
|
187
|
+
const port = await pickFreePort(searchBase + searchOffset, reserved);
|
|
188
|
+
portBySlug.set(fn.slug, port);
|
|
189
|
+
reserved.add(port);
|
|
190
|
+
searchOffset = port - searchBase + 1;
|
|
191
|
+
}
|
|
192
|
+
const assigned = functions.map((fn) => {
|
|
193
|
+
const port = portBySlug.get(fn.slug);
|
|
194
|
+
if (port === void 0) throw new Error(`planFunctionsToUnits: no port for ${fn.slug}`);
|
|
195
|
+
return {
|
|
196
|
+
fn,
|
|
197
|
+
port
|
|
198
|
+
};
|
|
166
199
|
});
|
|
200
|
+
const overlay = localFunctionUrlEnv(assigned.map(({ fn, port }) => ({
|
|
201
|
+
slug: fn.slug,
|
|
202
|
+
port
|
|
203
|
+
})));
|
|
204
|
+
const branchEnv = {
|
|
205
|
+
...neonEnv,
|
|
206
|
+
...overlay
|
|
207
|
+
};
|
|
208
|
+
return assigned.map(({ fn, port }) => plannedToUnit(fn, branchEnv, {
|
|
209
|
+
mode: "explicit",
|
|
210
|
+
port
|
|
211
|
+
}, overlay));
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Local invocation URLs for functions `neon dev` is serving, so siblings call each
|
|
215
|
+
* other on localhost instead of the production compute host.
|
|
216
|
+
*/
|
|
217
|
+
const localFunctionUrlEnv = (functions) => {
|
|
218
|
+
const out = {};
|
|
219
|
+
for (const fn of functions) out[functionBaseUrlKey(fn.slug)] = `http://localhost:${fn.port}`;
|
|
220
|
+
return out;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* `--source` serves one file. If that file is a neon.ts function, rewrite only that
|
|
224
|
+
* slug to localhost — this process is not listening for the others.
|
|
225
|
+
*/
|
|
226
|
+
const sourceFunctionUrlOverlay = async (match, cliPort) => {
|
|
227
|
+
const portSpec = portFromProps(cliPort);
|
|
228
|
+
if (match === void 0) return {
|
|
229
|
+
overlay: {},
|
|
230
|
+
port: portSpec
|
|
231
|
+
};
|
|
232
|
+
const port = portSpec.mode === "explicit" ? portSpec.port : match.port ?? await pickFreePort(portSpec.from);
|
|
233
|
+
return {
|
|
234
|
+
overlay: localFunctionUrlEnv([{
|
|
235
|
+
slug: match.slug,
|
|
236
|
+
port
|
|
237
|
+
}]),
|
|
238
|
+
port: {
|
|
239
|
+
mode: "explicit",
|
|
240
|
+
port
|
|
241
|
+
}
|
|
242
|
+
};
|
|
167
243
|
};
|
|
244
|
+
const pickFreePort = (from, reserved = /* @__PURE__ */ new Set()) => new Promise((resolvePort, reject) => {
|
|
245
|
+
const tryPort = (port) => {
|
|
246
|
+
if (port > 65535) {
|
|
247
|
+
reject(/* @__PURE__ */ new Error(`No free port at or above ${from}`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (reserved.has(port)) {
|
|
251
|
+
tryPort(port + 1);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const server = createServer();
|
|
255
|
+
server.once("error", (err) => {
|
|
256
|
+
if (err.code === "EADDRINUSE") {
|
|
257
|
+
tryPort(port + 1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
reject(err);
|
|
261
|
+
});
|
|
262
|
+
server.listen(port, "127.0.0.1", () => {
|
|
263
|
+
const addr = server.address();
|
|
264
|
+
const bound = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
265
|
+
server.close((closeErr) => {
|
|
266
|
+
if (closeErr) reject(closeErr);
|
|
267
|
+
else resolvePort(bound);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
tryPort(from);
|
|
272
|
+
});
|
|
168
273
|
/**
|
|
169
274
|
* Resolve the selected branch id from props, if any. Best-effort: a failure here only
|
|
170
275
|
* means env injection is skipped, so it never throws.
|
|
@@ -211,19 +316,11 @@ const portFromProps = (port) => {
|
|
|
211
316
|
*/
|
|
212
317
|
const devBundleDir = (cwd, slug) => slug === void 0 ? join(cwd, "node_modules", ".neon-dev") : join(cwd, "node_modules", ".neon-dev", slug);
|
|
213
318
|
/**
|
|
214
|
-
* Translate a {@link PlannedFunction} into a {@link ServedUnit}.
|
|
215
|
-
*
|
|
216
|
-
* - no `dev.port`: search for a free port (base coordinated by the caller).
|
|
319
|
+
* Translate a {@link PlannedFunction} into a {@link ServedUnit}. The bind port is
|
|
320
|
+
* already chosen by the caller so sibling localhost URLs can be overlaid first.
|
|
217
321
|
* Per-function neon.ts env layers over the shared branch env.
|
|
218
322
|
*/
|
|
219
|
-
const plannedToUnit = (fn, branchEnv,
|
|
220
|
-
const port = fn.port !== void 0 ? {
|
|
221
|
-
mode: "explicit",
|
|
222
|
-
port: fn.port
|
|
223
|
-
} : {
|
|
224
|
-
mode: "search",
|
|
225
|
-
from: searchBase
|
|
226
|
-
};
|
|
323
|
+
const plannedToUnit = (fn, branchEnv, port, overlay = {}) => {
|
|
227
324
|
const childEnv = buildChildEnv({
|
|
228
325
|
...branchEnv,
|
|
229
326
|
...fn.env
|
|
@@ -244,6 +341,7 @@ const plannedToUnit = (fn, branchEnv, searchBase) => {
|
|
|
244
341
|
source: fn.source,
|
|
245
342
|
port: fn.port ?? null,
|
|
246
343
|
env: fn.env,
|
|
344
|
+
overlay,
|
|
247
345
|
externalPackages: fn.externalPackages ?? null,
|
|
248
346
|
bundler: typeof fn.bundler === "function" ? "custom" : fn.bundler ?? null
|
|
249
347
|
})
|
|
@@ -442,7 +540,7 @@ const reconcileOnce = async (running, replan, ops) => {
|
|
|
442
540
|
if (ops.isShuttingDown()) return;
|
|
443
541
|
let desired;
|
|
444
542
|
try {
|
|
445
|
-
desired = await replan(nextSearchBase(running));
|
|
543
|
+
desired = await replan(nextSearchBase(running), keepPortsFromRunning(running));
|
|
446
544
|
} catch (err) {
|
|
447
545
|
log.info(chalk.red("neon.ts change ignored: ") + (err instanceof Error ? err.message : String(err)) + chalk.dim(" (fix it and save again)"));
|
|
448
546
|
return;
|
|
@@ -470,10 +568,23 @@ const reconcileOnce = async (running, replan, ops) => {
|
|
|
470
568
|
}
|
|
471
569
|
}
|
|
472
570
|
};
|
|
571
|
+
const keepPortsFromRunning = (running) => {
|
|
572
|
+
const keep = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const r of running) {
|
|
574
|
+
if (r.unit.slug === null) continue;
|
|
575
|
+
const bound = r.boundPort;
|
|
576
|
+
if (bound !== null) {
|
|
577
|
+
keep.set(r.unit.slug, bound);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const pending = Number(r.unit.childEnv.NEON_DEV_PORT);
|
|
581
|
+
if (Number.isFinite(pending) && pending > 0) keep.set(r.unit.slug, pending);
|
|
582
|
+
}
|
|
583
|
+
return keep;
|
|
584
|
+
};
|
|
473
585
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* from here, so it never fails even if this guess is taken — it just keeps things tidy.
|
|
586
|
+
* Start pickFreePort above every port live units already bound, so a hot-added
|
|
587
|
+
* function is not probed on a port a sibling is serving.
|
|
477
588
|
*/
|
|
478
589
|
const nextSearchBase = (running) => {
|
|
479
590
|
let max = 8786;
|
|
@@ -757,4 +868,4 @@ const resolveRuntimePath = () => {
|
|
|
757
868
|
return candidate;
|
|
758
869
|
};
|
|
759
870
|
//#endregion
|
|
760
|
-
export { builder, command, describe, devBundleDir, devEnvContext, diffUnits, formatEnvSummary, handler, reportDevCredential, dev_exports as t };
|
|
871
|
+
export { builder, command, describe, devBundleDir, devEnvContext, diffUnits, formatEnvSummary, handler, localFunctionUrlEnv, planFunctionsToUnits, reportDevCredential, sourceFunctionUrlOverlay, dev_exports as t };
|
package/dist/commands/env.js
CHANGED
|
@@ -4,11 +4,11 @@ import { ensureGitignored } from "../context.js";
|
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
|
|
6
6
|
import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
7
|
-
import { a as envKeysForSelection, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as
|
|
7
|
+
import { a as envKeysForSelection, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, o as envPullFlagValue, r as ENV_PULL_KEY_HELP, v as isFunctionBaseUrlKey } from "../_chunks/env_services-CAWrZTWa.js";
|
|
8
8
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
9
9
|
import { warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
10
10
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
11
|
-
import { a as resolveNeonEnvVars } from "../_chunks/env-
|
|
11
|
+
import { a as resolveNeonEnvVars } from "../_chunks/env-CZmstYrY.js";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
//#region src/commands/env.ts
|
|
@@ -51,7 +51,7 @@ const builder = (argv) => argv.usage("$0 env <sub-command> [options]").options({
|
|
|
51
51
|
}),
|
|
52
52
|
env: {
|
|
53
53
|
alias: "e",
|
|
54
|
-
describe: `Pull only these individual variables: ${
|
|
54
|
+
describe: `Pull only these individual variables: ${ENV_PULL_KEY_HELP}. Repeat the flag or comma-separate. Overrides neon.ts. Combines with --service as a union; it never narrows a service bundle.`,
|
|
55
55
|
type: "array",
|
|
56
56
|
string: true
|
|
57
57
|
}
|
|
@@ -59,19 +59,19 @@ const builder = (argv) => argv.usage("$0 env <sub-command> [options]").options({
|
|
|
59
59
|
"",
|
|
60
60
|
"What gets pulled, in precedence order:",
|
|
61
61
|
" 1. --service / --env, when given — their union, ignoring neon.ts.",
|
|
62
|
-
" 2. neon.ts, when this directory has one
|
|
62
|
+
" 2. neon.ts, when this directory has one — including derived function",
|
|
63
|
+
" URLs (the function does not have to be deployed).",
|
|
63
64
|
" 3. Otherwise everything the branch has, plus the AI Gateway —",
|
|
64
65
|
" which mints a branch credential for it.",
|
|
65
66
|
"",
|
|
66
67
|
"The pull bundled into link / checkout / config apply follows 2 and 3",
|
|
67
68
|
"without the AI Gateway, so it never mints a credential you did not ask",
|
|
68
69
|
"for. Run `env pull` to add it."
|
|
69
|
-
].join("\n")).example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)").example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file").example("$0 env pull -s ai-gateway -s postgres", "Pull only the AI Gateway and Postgres variables").example("$0 env pull -e DATABASE_URL", "Pull only the pooled Postgres connection string").example("$0 env pull -s auth -e DATABASE_URL", "Pull all Auth variables plus DATABASE_URL").strict(), async (args) => {
|
|
70
|
+
].join("\n")).example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)").example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file").example("$0 env pull -s ai-gateway -s postgres", "Pull only the AI Gateway and Postgres variables").example("$0 env pull -e DATABASE_URL", "Pull only the pooled Postgres connection string").example("$0 env pull -s auth -e DATABASE_URL", "Pull all Auth variables plus DATABASE_URL").example("$0 env pull -s functions", "Pull NEON_FUNCTION_<SLUG>_BASE_URL for every deployed function").example("$0 env pull -e NEON_FUNCTION_HELLO_BASE_URL", "Write that function's production invocation URL (no deploy required)").strict(), async (args) => {
|
|
70
71
|
const rawServices = servicesFlagValue(args.service);
|
|
71
72
|
const rawEnvKeys = envPullFlagValue(args.env);
|
|
72
73
|
const services = rawServices ? parseServices(rawServices, {
|
|
73
74
|
allowed: ENV_PULL_SERVICES,
|
|
74
|
-
whyUnavailable: ENV_PULL_UNAVAILABLE,
|
|
75
75
|
flag: "--service",
|
|
76
76
|
onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
|
|
77
77
|
}) : void 0;
|
|
@@ -127,16 +127,17 @@ const pull = async (props, opts = {}) => {
|
|
|
127
127
|
...props.services ? { services: props.services } : {},
|
|
128
128
|
...props.envKeys ? { envKeys: props.envKeys } : {},
|
|
129
129
|
...opts.implyAiGateway ? { implyAiGateway: true } : {},
|
|
130
|
+
omitUnsetFunctionEnv: true,
|
|
130
131
|
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
131
132
|
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
132
133
|
...props.runtimeApi ? { api: props.runtimeApi } : {}
|
|
133
134
|
});
|
|
134
|
-
const neonVars = pickSelectedVars(pickNeonVars(vars), selectedKeys);
|
|
135
|
+
const neonVars = pickSelectedVars(pickNeonVars(vars), selectedKeys, props.services);
|
|
135
136
|
if (Object.keys(neonVars).length === 0) {
|
|
136
137
|
log.info("No Neon env variables to pull for this branch (no DATABASE_URL or enabled Auth / Data API).");
|
|
137
138
|
return { status: "empty" };
|
|
138
139
|
}
|
|
139
|
-
const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(selectedKeys, unreachedButCurrent(skipped, existingEnv, branchId)) });
|
|
140
|
+
const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(selectedKeys, unreachedButCurrent(skipped, existingEnv, branchId), existingEnv, props.services, props.envKeys) });
|
|
140
141
|
log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
|
|
141
142
|
if (removed.length > 0) log.info("Removed %d stale Neon variable%s not enabled on this branch: %s", removed.length, removed.length === 1 ? "" : "s", removed.join(", "));
|
|
142
143
|
if (credential?.issued) {
|
|
@@ -164,42 +165,32 @@ const pull = async (props, opts = {}) => {
|
|
|
164
165
|
...skipped && skipped.length > 0 ? { skipped } : {}
|
|
165
166
|
};
|
|
166
167
|
};
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
* {@link unreachedButCurrent}.
|
|
174
|
-
*/
|
|
175
|
-
const managedKeysFor = (selectedKeys, unreached) => {
|
|
176
|
-
const owned = selectedKeys ? NEON_OWNED_ENV_KEYS.filter((key) => selectedKeys.includes(key)) : [...NEON_OWNED_ENV_KEYS];
|
|
168
|
+
/** Prevents scoped or unavailable services from pruning values they did not resolve. */
|
|
169
|
+
const managedKeysFor = (selectedKeys, unreached, existingEnv, services, envKeys) => {
|
|
170
|
+
const existingFunctionKeys = Object.keys(existingEnv).filter(isFunctionBaseUrlKey);
|
|
171
|
+
const namedFunctionKeys = (envKeys ?? []).filter(isFunctionBaseUrlKey);
|
|
172
|
+
const ownedFunctionKeys = selectedKeys === void 0 || (services?.includes("functions") ?? false) ? existingFunctionKeys : namedFunctionKeys;
|
|
173
|
+
const owned = selectedKeys ? [...NEON_OWNED_ENV_KEYS.filter((key) => selectedKeys.includes(key)), ...ownedFunctionKeys] : [...NEON_OWNED_ENV_KEYS, ...ownedFunctionKeys];
|
|
177
174
|
if (unreached.length === 0) return owned;
|
|
178
175
|
const keep = new Set(ownedEnvServiceKeys(unreached));
|
|
179
|
-
return owned.filter((key) =>
|
|
176
|
+
return owned.filter((key) => {
|
|
177
|
+
if (unreached.includes("functions") && isFunctionBaseUrlKey(key)) return false;
|
|
178
|
+
return !keep.has(key);
|
|
179
|
+
});
|
|
180
180
|
};
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* Failing to reach a service is not evidence that the branch stopped having it:
|
|
186
|
-
* `PLATFORM_FEATURE_UNAVAILABLE` covers a transient incident as well as a project that
|
|
187
|
-
* genuinely lacks the feature, and pruning would delete a token whose secret exists nowhere
|
|
188
|
-
* else and strand the live credential behind it. But that only argues for keeping *this
|
|
189
|
-
* branch's* values. Variables left over from another branch are stale by definition, and
|
|
190
|
-
* keeping those would leave an app pointed at the wrong branch's gateway — a worse failure
|
|
191
|
-
* than losing a token, because it is silent.
|
|
192
|
-
*
|
|
193
|
-
* The gateway is the only service that can be unreached (only it is implied rather than
|
|
194
|
-
* observed), and its base URL is branch-scoped, so the persisted URL is what tells the two
|
|
195
|
-
* cases apart. Anything that does not resolve to this branch's gateway host is pruned, which
|
|
196
|
-
* is the safe direction: a stale entry costs a re-pull, a wrongly-kept one silently misroutes
|
|
197
|
-
* traffic.
|
|
182
|
+
* Endpoint failure is not evidence that existing values are stale; the gateway host check
|
|
183
|
+
* avoids preserving credentials for a different branch.
|
|
198
184
|
*/
|
|
199
185
|
const unreachedButCurrent = (skipped, existingEnv, branchId) => {
|
|
200
|
-
if (!skipped
|
|
201
|
-
const
|
|
202
|
-
|
|
186
|
+
if (!skipped) return [];
|
|
187
|
+
const keep = [];
|
|
188
|
+
if (skipped.includes("ai-gateway")) {
|
|
189
|
+
const baseUrl = existingEnv[NEON_ENV_VAR_KEYS.aiGateway.baseUrl];
|
|
190
|
+
if (baseUrl !== void 0 && isBranchGatewayUrl(baseUrl, branchId)) keep.push("ai-gateway");
|
|
191
|
+
}
|
|
192
|
+
if (skipped.includes("functions")) keep.push("functions");
|
|
193
|
+
return keep;
|
|
203
194
|
};
|
|
204
195
|
/**
|
|
205
196
|
* Whether a persisted `NEON_AI_GATEWAY_BASE_URL` addresses `branchId`'s gateway.
|
|
@@ -216,10 +207,11 @@ const isBranchGatewayUrl = (baseUrl, branchId) => URL.canParse(baseUrl) && new U
|
|
|
216
207
|
* supporting values that were not selected (the direct connection URI derives the AI Gateway
|
|
217
208
|
* host, for example); those must not land in the target file.
|
|
218
209
|
*/
|
|
219
|
-
const pickSelectedVars = (vars, selectedKeys) => {
|
|
210
|
+
const pickSelectedVars = (vars, selectedKeys, services) => {
|
|
220
211
|
if (!selectedKeys) return vars;
|
|
221
212
|
const wanted = new Set(selectedKeys);
|
|
222
|
-
|
|
213
|
+
const keepFunctionUrls = services?.includes("functions") ?? false;
|
|
214
|
+
return Object.fromEntries(Object.entries(vars).filter(([key]) => wanted.has(key) || keepFunctionUrls && isFunctionBaseUrlKey(key)));
|
|
223
215
|
};
|
|
224
216
|
/**
|
|
225
217
|
* Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
|
|
@@ -254,10 +246,7 @@ Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\`
|
|
|
254
246
|
*/
|
|
255
247
|
const pickNeonVars = (vars) => {
|
|
256
248
|
const out = {};
|
|
257
|
-
for (const name of NEON_VAR_NAMES)
|
|
258
|
-
const value = vars[name];
|
|
259
|
-
if (value !== void 0) out[name] = value;
|
|
260
|
-
}
|
|
249
|
+
for (const [name, value] of Object.entries(vars)) if (NEON_VAR_NAMES.some((key) => key === name) || isFunctionBaseUrlKey(name)) out[name] = value;
|
|
261
250
|
return out;
|
|
262
251
|
};
|
|
263
252
|
//#endregion
|
|
@@ -2,11 +2,13 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { isNeonApiError, retryOnLock } from "../api.js";
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
|
+
import { deleteCustomDomain, listCustomDomains, registerCustomDomain } from "../custom_domains_api.js";
|
|
5
6
|
import { createDeployment, deleteFunction, getFunction, listFunctions } from "../functions_api.js";
|
|
6
7
|
import { writer } from "../writer.js";
|
|
7
8
|
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
8
9
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
9
10
|
import { zipBundle } from "../utils/zip.js";
|
|
11
|
+
import { DEPLOY_COMMANDS_EPILOGUE } from "./deploy_help.js";
|
|
10
12
|
import { statSync } from "node:fs";
|
|
11
13
|
import { dirname } from "node:path";
|
|
12
14
|
import { FUNCTION_SOURCE_ENTRIES } from "@neon/config";
|
|
@@ -26,6 +28,13 @@ const FUNCTION_FIELDS = [
|
|
|
26
28
|
"created_at"
|
|
27
29
|
];
|
|
28
30
|
const FUNCTIONS_LIST_LIMIT = 100;
|
|
31
|
+
const CUSTOM_DOMAINS_LIST_LIMIT = 100;
|
|
32
|
+
const CUSTOM_DOMAIN_FIELDS = [
|
|
33
|
+
"domain",
|
|
34
|
+
"entity_type",
|
|
35
|
+
"entity_id",
|
|
36
|
+
"cname_target"
|
|
37
|
+
];
|
|
29
38
|
const LIST_TABLE_FIELDS = [
|
|
30
39
|
"slug",
|
|
31
40
|
"name",
|
|
@@ -107,7 +116,7 @@ const builder = (argv) => argv.usage("$0 function <sub-command> [options]").opti
|
|
|
107
116
|
type: "boolean",
|
|
108
117
|
default: true
|
|
109
118
|
}
|
|
110
|
-
}), (args) => deploy(args)).command("list", "List functions on the branch", (yargs) => yargs, (args) => list(args)).command("get <slug>", "Show a function's details", (yargs) => yargs.positional("slug", {
|
|
119
|
+
}).epilogue(DEPLOY_COMMANDS_EPILOGUE), (args) => deploy(args)).command("list", "List functions on the branch", (yargs) => yargs, (args) => list(args)).command("get <slug>", "Show a function's details", (yargs) => yargs.positional("slug", {
|
|
111
120
|
describe: "Function slug",
|
|
112
121
|
type: "string",
|
|
113
122
|
demandOption: true
|
|
@@ -120,7 +129,19 @@ const builder = (argv) => argv.usage("$0 function <sub-command> [options]").opti
|
|
|
120
129
|
describe: "Function slug",
|
|
121
130
|
type: "string",
|
|
122
131
|
demandOption: true
|
|
123
|
-
}), (args) => deleteFn(args))
|
|
132
|
+
}), (args) => deleteFn(args)).command(["domains", "domain"], "Manage custom domains on the branch (beta)", (yargs) => yargs.demandCommand(1).command("list", "List custom domains on the branch", (yargs) => yargs, (args) => listDomains(args)).command("register <domain>", "Point a domain you already own at a function on the branch", (yargs) => yargs.positional("domain", {
|
|
133
|
+
describe: "Domain you already own (for example docs.example.com)",
|
|
134
|
+
type: "string",
|
|
135
|
+
demandOption: true
|
|
136
|
+
}).options({ slug: {
|
|
137
|
+
describe: "Function slug this domain should point at",
|
|
138
|
+
type: "string",
|
|
139
|
+
demandOption: true
|
|
140
|
+
} }), (args) => registerDomain(args)).command("delete <domain>", "Delete a custom domain from the branch", (yargs) => yargs.positional("domain", {
|
|
141
|
+
describe: "Custom domain",
|
|
142
|
+
type: "string",
|
|
143
|
+
demandOption: true
|
|
144
|
+
}), (args) => deleteDomain(args)));
|
|
124
145
|
const handler = (args) => {
|
|
125
146
|
return args;
|
|
126
147
|
};
|
|
@@ -314,5 +335,45 @@ const list = async (props) => {
|
|
|
314
335
|
emptyMessage: "No functions found on this branch."
|
|
315
336
|
});
|
|
316
337
|
};
|
|
338
|
+
const listDomains = async (props) => {
|
|
339
|
+
const branchId = await branchIdFromProps(props);
|
|
340
|
+
const domains = [];
|
|
341
|
+
let cursor;
|
|
342
|
+
for (;;) {
|
|
343
|
+
const page = await listCustomDomains(props.apiClient, props.projectId, branchId, {
|
|
344
|
+
cursor,
|
|
345
|
+
limit: CUSTOM_DOMAINS_LIST_LIMIT
|
|
346
|
+
});
|
|
347
|
+
domains.push(...page.custom_domains);
|
|
348
|
+
if (!page.next || page.next === cursor) break;
|
|
349
|
+
cursor = page.next;
|
|
350
|
+
}
|
|
351
|
+
writer(props).end(domains, {
|
|
352
|
+
fields: CUSTOM_DOMAIN_FIELDS,
|
|
353
|
+
emptyMessage: "No custom domains found on this branch."
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
const registerDomain = async (props) => {
|
|
357
|
+
if (!SLUG_PATTERN.test(props.slug)) throw new Error(`Invalid function slug "${props.slug}". ${SLUG_HELP}`);
|
|
358
|
+
const branchId = await branchIdFromProps(props);
|
|
359
|
+
const registered = await retryOnLock(() => registerCustomDomain(props.apiClient, props.projectId, branchId, {
|
|
360
|
+
domain: props.domain,
|
|
361
|
+
entity_type: "function",
|
|
362
|
+
entity_id: props.slug
|
|
363
|
+
}));
|
|
364
|
+
writer(props).end(registered, { fields: CUSTOM_DOMAIN_FIELDS });
|
|
365
|
+
if (registered.cname_target) log.info(`CNAME ${registered.domain} to ${registered.cname_target}`);
|
|
366
|
+
else log.info(`No CNAME target for ${registered.domain}; this region has no custom-domains front door.`);
|
|
367
|
+
};
|
|
368
|
+
const deleteDomain = async (props) => {
|
|
369
|
+
const branchId = await branchIdFromProps(props);
|
|
370
|
+
try {
|
|
371
|
+
await retryOnLock(() => deleteCustomDomain(props.apiClient, props.projectId, branchId, props.domain));
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (isNeonApiError(err) && err.status === 404) throw new Error(`Custom domain "${props.domain}" not found on branch ${branchId}.`);
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
log.info(`Custom domain ${props.domain} deleted from branch ${branchId}`);
|
|
377
|
+
};
|
|
317
378
|
//#endregion
|
|
318
379
|
export { aliases, builder, command, describe, handler, functions_exports as t };
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { t as writeSecretFile } from "../_chunks/secure_file-BucZj4yQ.js";
|
|
3
|
-
import { c as describeScope, d as scopeOf, l as interpretCredentials, n as KEYRING_SERVICE, o as apiKeyCredentials, r as keyringAccount, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-
|
|
3
|
+
import { c as describeScope, d as scopeOf, l as interpretCredentials, n as KEYRING_SERVICE, o as apiKeyCredentials, r as keyringAccount, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-D8AR-jvB.js";
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { getApiClient, isNeonApiError } from "../api.js";
|
|
7
|
-
import { a as isOwnedCredentialPath, n as credentialsPath } from "../_chunks/paths-DMq0Lt7a.js";
|
|
8
7
|
import { setAuthContext } from "../auth_context.js";
|
|
8
|
+
import { a as isOwnedCredentialPath, n as credentialsPath } from "../_chunks/paths-DMq0Lt7a.js";
|
|
9
9
|
import { _ as upsertProfile, a as canDropProfilesFile, c as listProfiles, d as newProfileLocation, f as profilesFilePath, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, o as credentialsDisplay, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, u as locationOf, v as CRED_STORAGE_FILE, y as CRED_STORAGE_KEYRING } from "../_chunks/profiles-CvnFEQyd.js";
|
|
10
10
|
import { auth, revokeToken } from "../auth.js";
|
|
11
11
|
import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
|
package/dist/config_services.js
CHANGED
|
@@ -14,5 +14,40 @@ const declaredNeonServices = (config) => {
|
|
|
14
14
|
if (isToggleEnabled(config.preview?.aiGateway)) services.push("ai-gateway");
|
|
15
15
|
return services;
|
|
16
16
|
};
|
|
17
|
+
const dataApiSettingsToSnake = (settings) => {
|
|
18
|
+
const out = {};
|
|
19
|
+
if (settings.dbAggregatesEnabled !== void 0) out.db_aggregates_enabled = settings.dbAggregatesEnabled;
|
|
20
|
+
if (settings.dbAnonRole !== void 0) out.db_anon_role = settings.dbAnonRole;
|
|
21
|
+
if (settings.dbExtraSearchPath !== void 0) out.db_extra_search_path = settings.dbExtraSearchPath;
|
|
22
|
+
if (settings.dbMaxRows !== void 0) out.db_max_rows = settings.dbMaxRows;
|
|
23
|
+
if (settings.dbSchemas !== void 0) out.db_schemas = settings.dbSchemas;
|
|
24
|
+
if (settings.jwtRoleClaimKey !== void 0) out.jwt_role_claim_key = settings.jwtRoleClaimKey;
|
|
25
|
+
if (settings.jwtCacheMaxLifetime !== void 0) out.jwt_cache_max_lifetime = settings.jwtCacheMaxLifetime;
|
|
26
|
+
if (settings.openapiMode !== void 0) out.openapi_mode = settings.openapiMode;
|
|
27
|
+
if (settings.serverCorsAllowedOrigins !== void 0) out.server_cors_allowed_origins = settings.serverCorsAllowedOrigins;
|
|
28
|
+
if (settings.serverTimingEnabled !== void 0) out.server_timing_enabled = settings.serverTimingEnabled;
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
/** Omitting this body preserves the server heuristic used by `--service data-api`. */
|
|
32
|
+
const claimableDataApiCreateBody = (config) => {
|
|
33
|
+
if (!isToggleEnabled(config.dataApi)) return void 0;
|
|
34
|
+
const input = config.dataApi;
|
|
35
|
+
if (typeof input !== "object") return { auth_provider: "neon_auth" };
|
|
36
|
+
const settings = input.settings ? dataApiSettingsToSnake(input.settings) : void 0;
|
|
37
|
+
const withSettings = (body) => settings && Object.keys(settings).length > 0 ? {
|
|
38
|
+
...body,
|
|
39
|
+
settings
|
|
40
|
+
} : body;
|
|
41
|
+
if (input.authProvider === "external") {
|
|
42
|
+
if (input.jwksUrl === void 0 || input.jwksUrl.length === 0) throw new Error("neon.ts dataApi.authProvider \"external\" requires dataApi.jwksUrl.");
|
|
43
|
+
return withSettings({
|
|
44
|
+
auth_provider: "external",
|
|
45
|
+
jwks_url: input.jwksUrl,
|
|
46
|
+
...input.providerName !== void 0 ? { provider_name: input.providerName } : {},
|
|
47
|
+
...input.jwtAudience !== void 0 ? { jwt_audience: input.jwtAudience } : {}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return withSettings({ auth_provider: "neon_auth" });
|
|
51
|
+
};
|
|
17
52
|
//#endregion
|
|
18
|
-
export { declaredNeonServices };
|
|
53
|
+
export { claimableDataApiCreateBody, declaredNeonServices };
|
package/dist/credential_io.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as storeFor } from "./_chunks/credential_io-
|
|
1
|
+
import { t as storeFor } from "./_chunks/credential_io-D8AR-jvB.js";
|
|
2
2
|
export { storeFor };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/custom_domains_api.ts
|
|
2
|
+
const customDomainsPath = (projectId, branchId) => `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/custom-domains`;
|
|
3
|
+
const listCustomDomains = async (apiClient, projectId, branchId, { cursor, limit } = {}) => {
|
|
4
|
+
const { data } = await apiClient.request({
|
|
5
|
+
path: customDomainsPath(projectId, branchId),
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: {
|
|
8
|
+
cursor,
|
|
9
|
+
limit
|
|
10
|
+
},
|
|
11
|
+
secure: true,
|
|
12
|
+
format: "json"
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
custom_domains: data.custom_domains ?? [],
|
|
16
|
+
next: data.pagination?.next
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const registerCustomDomain = async (apiClient, projectId, branchId, input) => {
|
|
20
|
+
const { data } = await apiClient.request({
|
|
21
|
+
path: customDomainsPath(projectId, branchId),
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: input,
|
|
24
|
+
format: "json",
|
|
25
|
+
secure: true
|
|
26
|
+
});
|
|
27
|
+
return data;
|
|
28
|
+
};
|
|
29
|
+
const deleteCustomDomain = async (apiClient, projectId, branchId, domain) => {
|
|
30
|
+
await apiClient.request({
|
|
31
|
+
path: `${customDomainsPath(projectId, branchId)}/${encodeURIComponent(domain)}`,
|
|
32
|
+
method: "DELETE",
|
|
33
|
+
secure: true
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { deleteCustomDomain, listCustomDomains, registerCustomDomain };
|
package/dist/dev/env.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as resolveNeonEnvVars, i as resolveDevEnv, n as MissingBranchContextError, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-
|
|
1
|
+
import { a as resolveNeonEnvVars, i as resolveDevEnv, n as MissingBranchContextError, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-CZmstYrY.js";
|
|
2
2
|
export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, resolveDevEnv, resolveNeonEnvVars };
|