neon 4.13.1 → 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 +51 -9
- 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/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 +8 -6
package/README.md
CHANGED
|
@@ -78,9 +78,36 @@ neon claim create --service auth --service data-api
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
When the current directory has a `neon.ts`, `claim create` also requests every service
|
|
81
|
-
declared there. Explicit `--service` values are added to that set.
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
declared there. Explicit `--service` values are added to that set. If that policy enables
|
|
82
|
+
the Data API, identity includes a snake_case `data_api` object. A neon.ts with
|
|
83
|
+
`dataApi: { authProvider: "external", jwksUrl: "https://idp.example.com/jwks.json" }`
|
|
84
|
+
sends:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"type": "anonymous",
|
|
89
|
+
"capabilities": ["postgres", "data_api"],
|
|
90
|
+
"source": "neon_cli",
|
|
91
|
+
"data_api": {
|
|
92
|
+
"auth_provider": "external",
|
|
93
|
+
"jwks_url": "https://idp.example.com/jwks.json"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`neon claim create --service data-api` with no neon.ts Data API block omits `data_api`:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"type": "anonymous",
|
|
103
|
+
"capabilities": ["postgres", "data_api"],
|
|
104
|
+
"source": "neon_cli"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Object Storage, Functions, and the AI Gateway are sent to the service so demand is
|
|
109
|
+
recorded, but are reported as unavailable until the project is claimed; the CLI does
|
|
110
|
+
not silently remove them.
|
|
84
111
|
|
|
85
112
|
The command writes:
|
|
86
113
|
|
|
@@ -467,8 +494,8 @@ The human-readable summary line goes to stderr and the diff body to stdout, so `
|
|
|
467
494
|
**What gets pulled**, in precedence order:
|
|
468
495
|
|
|
469
496
|
1. **`--service` and/or `--env`**, when you pass either — their union is the complete selection, ignoring `neon.ts` and unselected branch variables. `--service` adds a service's complete variable bundle; `--env` adds only the individual variables you name.
|
|
470
|
-
2. **`neon.ts`**, when the working directory has one — the policy is the source of truth, same as `neon dev` and `neon deploy`.
|
|
471
|
-
3. **Everything the branch has** otherwise — Postgres, Neon Auth, the Data API,
|
|
497
|
+
2. **`neon.ts`**, when the working directory has one — the policy is the source of truth, same as `neon dev` and `neon deploy`. Declared function URLs are derived from the branch connection host; the function does not have to be deployed.
|
|
498
|
+
3. **Everything the branch has** otherwise — Postgres, Neon Auth, the Data API, object storage, and function invocation URLs read back from the branch, plus the AI Gateway. The gateway has no branch-level state to read back, so a bare `env pull` asks for it rather than detecting it and may mint a branch credential. To leave it out, name only what you do want with `--service` and/or `--env`.
|
|
472
499
|
|
|
473
500
|
If the gateway can't be resolved, it is dropped with a warning and the rest of the pull still lands. Gateway variables already in your file for *this* branch are left alone — a pull that couldn't reach the gateway is no evidence the branch has stopped having one — while ones left over from a different branch are pruned like any other stale value.
|
|
474
501
|
|
|
@@ -492,6 +519,10 @@ neon env pull -e DATABASE_URL,NEON_AUTH_BASE_URL
|
|
|
492
519
|
|
|
493
520
|
# The selectors compose as a union: all Auth vars plus DATABASE_URL
|
|
494
521
|
neon env pull -s auth -e DATABASE_URL
|
|
522
|
+
|
|
523
|
+
# Function invocation URLs
|
|
524
|
+
neon env pull -s functions
|
|
525
|
+
neon env pull -e NEON_FUNCTION_HELLO_BASE_URL
|
|
495
526
|
```
|
|
496
527
|
|
|
497
528
|
Every services flag in the CLI takes those three spellings, the same value syntax, and the same service names — see [`config init --services`](#getting-a-neonts-config-init).
|
|
@@ -501,14 +532,15 @@ Every services flag in the CLI takes those three spellings, the same value synta
|
|
|
501
532
|
| `postgres` | `DATABASE_URL`, `DATABASE_URL_UNPOOLED` |
|
|
502
533
|
| `auth` | `NEON_AUTH_BASE_URL`, `NEON_AUTH_JWKS_URL` |
|
|
503
534
|
| `data-api` | `NEON_DATA_API_URL` |
|
|
535
|
+
| `functions` | `NEON_FUNCTION_<SLUG>_BASE_URL` for each deployed function |
|
|
504
536
|
| `object-storage` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINT_URL_S3`, `AWS_REGION` |
|
|
505
537
|
| `ai-gateway` | `NEON_AI_GATEWAY_TOKEN`, `NEON_AI_GATEWAY_BASE_URL` |
|
|
506
538
|
|
|
507
|
-
`-e, --env` accepts any variable in the table plus `NEON_BRANCH
|
|
539
|
+
`-e, --env` accepts any variable in the table plus `NEON_BRANCH`, or `NEON_FUNCTION_<SLUG>_BASE_URL` where `<SLUG>` is the function slug uppercased. It is case-sensitive and rejects unknown names rather than silently widening the pull. `NEON_BRANCH` is written by unscoped and service-scoped pulls because it is branch identity, not a service; an env-only pull writes it only when you select it.
|
|
508
540
|
|
|
509
541
|
`--env` never narrows `--service`: `neon env pull -s postgres -e DATABASE_URL` still pulls the complete Postgres bundle (`DATABASE_URL`, `DATABASE_URL_UNPOOLED`, and `NEON_BRANCH`). The two selectors always form a union.
|
|
510
542
|
|
|
511
|
-
**A scoped pull is scoped in both directions.** An unscoped `env pull` owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale `NEON_AUTH_*` lines. `--service` narrows that to the services you named, while `--env` narrows it to the exact keys you named, so `env pull -e DATABASE_URL` never touches `DATABASE_URL_UNPOOLED`. (`AWS_*` is never pruned by any pull: those names collide with credentials you may set yourself, so `env pull` only ever writes them.)
|
|
543
|
+
**A scoped pull is scoped in both directions.** An unscoped `env pull` owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale `NEON_AUTH_*` lines, and a branch without those functions prunes stale `NEON_FUNCTION_*_BASE_URL` lines. `--service` narrows that to the services you named, while `--env` narrows it to the exact keys you named, so `env pull -e DATABASE_URL` never touches `DATABASE_URL_UNPOOLED`. (`AWS_*` is never pruned by any pull: those names collide with credentials you may set yourself, so `env pull` only ever writes them.)
|
|
512
544
|
|
|
513
545
|
**A scoped pull also never revokes a credential.** Where an unscoped pull revokes the credential it replaces, a scoped one leaves the old one live — it can't tell which other variables still use it. It says so when it happens; revoke it in the Neon Console if nothing does.
|
|
514
546
|
|
|
@@ -526,7 +558,7 @@ Console), or drop auth from --service.
|
|
|
526
558
|
|
|
527
559
|
If you'd rather not keep env vars on disk, inject them at runtime instead with `neon-env run -- <your dev command>` (from `@neon/env`) or `neon dev`, and pass `--no-env-pull` to `link` / `checkout`.
|
|
528
560
|
|
|
529
|
-
**`neon dev` resolves the same set, by the same rules** — including the AI Gateway on a branch with no `neon.ts`. A function running locally gets
|
|
561
|
+
**`neon dev` resolves the same set, by the same rules** — including the AI Gateway on a branch with no `neon.ts`. A function running locally gets the same Postgres / Auth / Data API / storage / AI Gateway vars a deploy would inject. Each `NEON_FUNCTION_<SLUG>_BASE_URL` this process is serving is rewritten to `http://localhost:<port>`: every neon.ts function, or the matching slug when `--source` names one. `neon env pull` and `neon-env run` write the production URL (`https://<branchId>-<slug>.compute.…`). `dev` writes nothing, but it does *read* your `.env` / `.env.local` to reuse the branch credential behind the AI Gateway and object storage. Without a file to read from it issues one on every start and leaves the last one live — it has nowhere to keep it, and so cannot name it to revoke it. It says so when it happens; run `env pull` (or just `link` / `checkout`) once and restarts reuse the credential instead.
|
|
530
562
|
|
|
531
563
|
**Where `.neon` lives**: `link` writes `.neon` into the **current working directory** by default. If an existing `.neon` is found in any parent directory, that file is reused — so commands run from a sub-directory of a linked project still pick up the project's context. To pin the location explicitly, pass `--context-file <path>`.
|
|
532
564
|
|
|
@@ -677,6 +709,16 @@ neon deploy --branch my-feature --update-existing
|
|
|
677
709
|
|
|
678
710
|
Function deploys declared under `preview.functions` are bundled with esbuild by default. A directory `source` is discovered as `index.ts`, then `index.js`, then `index.mjs`. Set `bundler: "none"` to ship a prebuilt directory as-is. `neon function deploy --no-bundle` is the same switch without a `neon.ts`.
|
|
679
711
|
|
|
712
|
+
Point a domain you already own at a function with `neon function domains` (alias `domain`):
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
neon function domains register docs.example.com --slug api
|
|
716
|
+
neon function domains list
|
|
717
|
+
neon function domains delete docs.example.com
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Register prints the record, then an INFO line `CNAME <domain> to <cname_target>`. The domain goes live after that CNAME resolves and a certificate is issued on the first request.
|
|
721
|
+
|
|
680
722
|
When a package cannot be bundled — a native addon with no esbuild loader, or an optional peer dependency a library references on an untaken code path — list it in that function's `externalPackages` and the bundler leaves the import alone. `neon dev` honours the same list. It does not make the package resolvable in the deployed archive (there is no `node_modules` next to the bundle), so it only unblocks an import that is never evaluated — a dependency the handler actually calls has to be bundled, and a natively-backed one cannot be. See [`@neon/config`](../config/README.md#unbundleable-dependencies-externalpackages).
|
|
681
723
|
|
|
682
724
|
## Scaffold a project (`bootstrap`)
|
|
@@ -1224,7 +1266,7 @@ Id Name Project Created At Last Used At Last
|
|
|
1224
1266
|
| [me](https://neon.com/docs/reference/cli-me) | | Show current user |
|
|
1225
1267
|
| [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches |
|
|
1226
1268
|
| [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
|
|
1227
|
-
| function | `deploy`, `list`, `get`, `delete`
|
|
1269
|
+
| function | `deploy`, `list`, `get`, `delete`, `domains list`, `domains register`, `domains delete` | Manage Neon Functions |
|
|
1228
1270
|
| [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
|
|
1229
1271
|
| [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
|
|
1230
1272
|
| logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
|
|
@@ -236,7 +236,7 @@ const createCredentialStore = (dir, options = {}) => {
|
|
|
236
236
|
const assertKeyringWritable = (profile) => {
|
|
237
237
|
if (keyring === null) throw new KeyringUnavailableError(profile, "write");
|
|
238
238
|
};
|
|
239
|
-
const setKeyringOrRollback = (profile, credentials) => {
|
|
239
|
+
const setKeyringOrRollback = (profile, credentials, restorePrevious) => {
|
|
240
240
|
assertKeyringWritable(profile);
|
|
241
241
|
const kr = keyring;
|
|
242
242
|
if (kr === null) throw new KeyringUnavailableError(profile, "write");
|
|
@@ -256,7 +256,7 @@ const createCredentialStore = (dir, options = {}) => {
|
|
|
256
256
|
try {
|
|
257
257
|
if (kr.get("com.neon.neon-cli", account) === null) throw new Error(`Wrote credentials to the OS keyring for ${label} but could not read them back.`);
|
|
258
258
|
} catch (err) {
|
|
259
|
-
if (previous !== null) {
|
|
259
|
+
if (restorePrevious && previous !== null) {
|
|
260
260
|
try {
|
|
261
261
|
kr.set(KEYRING_SERVICE, account, previous);
|
|
262
262
|
} catch {
|
|
@@ -372,9 +372,9 @@ const createCredentialStore = (dir, options = {}) => {
|
|
|
372
372
|
profile: at.profile
|
|
373
373
|
};
|
|
374
374
|
};
|
|
375
|
-
const write = (at, credentials) => {
|
|
375
|
+
const write = (at, credentials, options) => {
|
|
376
376
|
if (at.storage === "keyring") {
|
|
377
|
-
setKeyringOrRollback(at.profile, credentials);
|
|
377
|
+
setKeyringOrRollback(at.profile, credentials, options?.restorePrevious !== false);
|
|
378
378
|
return {
|
|
379
379
|
credentials,
|
|
380
380
|
backend: CRED_STORAGE_KEYRING,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
2
|
import { getCliName } from "../utils/cli_name.js";
|
|
3
|
-
import { _ as
|
|
3
|
+
import { S as toEntries, _ as functionBaseUrlKey, a as envKeysForSelection, b as previewCredentialScopes, d as servicesForEnvKeys, f as NEON_ENV_VAR_KEYS, g as fetchEnvKeysState, h as credentialName, i as ENV_PULL_SERVICES, m as credentialEnvKeys, p as createApiFromOptions, u as serviceForEnvKey, v as isFunctionBaseUrlKey, x as resolveBranchPolicy, y as policyEnvKeys } from "./env_services-CAWrZTWa.js";
|
|
4
4
|
import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
|
|
5
5
|
import { credentialScopesSatisfied } from "@neon/config/v1";
|
|
6
|
-
import { ErrorCode as ErrorCode$1, createNeonApiFromOptions as createNeonApiFromOptions$1, isPlatformError, loadConfigFromFile } from "@neon/config";
|
|
6
|
+
import { ErrorCode as ErrorCode$1, createNeonApiFromOptions as createNeonApiFromOptions$1, isPlatformError as isPlatformError$1, loadConfigFromFile } from "@neon/config";
|
|
7
7
|
import { plan, pullConfig } from "@neon/config-runtime";
|
|
8
8
|
//#region ../../internals/env-core/dist/reuse-secrets.js
|
|
9
9
|
/**
|
|
@@ -45,7 +45,7 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
45
45
|
const { branch, desired } = await resolveBranchPolicy(config, options, api);
|
|
46
46
|
const allPolicyKeys = policyEnvKeys(desired);
|
|
47
47
|
const requested = requestedKeys ? new Set(requestedKeys) : null;
|
|
48
|
-
const selectedPolicyKeys = requested === null ? allPolicyKeys : allPolicyKeys.filter((key) => requested.has(key));
|
|
48
|
+
const selectedPolicyKeys = requested === null ? allPolicyKeys : [...allPolicyKeys.filter((key) => requested.has(key)), ...[...requested].filter(isFunctionBaseUrlKey).sort()];
|
|
49
49
|
const selected = new Set(selectedPolicyKeys);
|
|
50
50
|
const K = NEON_ENV_VAR_KEYS;
|
|
51
51
|
const storageCredentialSelected = (desired.preview?.buckets.length ?? 0) > 0 && (selected.has(K.storage.accessKeyId) || selected.has(K.storage.secretAccessKey));
|
|
@@ -55,15 +55,16 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
55
55
|
aiGateway: gatewayCredentialSelected
|
|
56
56
|
}).filter((key) => selected.has(key));
|
|
57
57
|
if (secretKeys.length === 0) {
|
|
58
|
-
const fetched = await
|
|
58
|
+
const fetched = await fetchEnvKeysState(config, fetchOptions, requested === null ? null : selectedPolicyKeys);
|
|
59
59
|
return {
|
|
60
|
-
vars: preferPersisted(toEntries(fetched), source),
|
|
60
|
+
vars: preferPersisted(toEntries(fetched.env), source),
|
|
61
61
|
credential: {
|
|
62
62
|
issued: false,
|
|
63
63
|
keys: [],
|
|
64
64
|
revoked: [],
|
|
65
65
|
superseded: []
|
|
66
|
-
}
|
|
66
|
+
},
|
|
67
|
+
...fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {}
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
const persisted = readPersistedSecrets(source);
|
|
@@ -83,13 +84,15 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
83
84
|
aiGateway: gatewayCredentialSelected
|
|
84
85
|
});
|
|
85
86
|
const keep = reusable !== null && credentialScopesSatisfied(reusable.scopes, scopes);
|
|
86
|
-
const fetchKeys = keep ? selectedPolicyKeys.filter((key) => !secretKeys.includes(key)) : selectedPolicyKeys;
|
|
87
|
-
const fetched = await
|
|
87
|
+
const fetchKeys = requested === null ? null : keep ? selectedPolicyKeys.filter((key) => !secretKeys.includes(key)) : selectedPolicyKeys;
|
|
88
|
+
const fetched = await fetchEnvKeysState(config, {
|
|
88
89
|
...fetchOptions,
|
|
89
90
|
branchId: branch.id,
|
|
90
|
-
api
|
|
91
|
+
api,
|
|
92
|
+
...keep && requested === null ? { omitKeys: secretKeys } : {}
|
|
91
93
|
}, fetchKeys);
|
|
92
|
-
const vars = preferPersisted(toEntries(fetched), source);
|
|
94
|
+
const vars = preferPersisted(toEntries(fetched.env), source);
|
|
95
|
+
const unavailable = fetched.functionUrlsUnavailable ? { functionUrlsUnavailable: true } : {};
|
|
93
96
|
if (keep) {
|
|
94
97
|
for (const key of secretKeys) {
|
|
95
98
|
const value = source[key];
|
|
@@ -102,7 +105,8 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
102
105
|
keys: secretKeys,
|
|
103
106
|
revoked: [],
|
|
104
107
|
superseded: []
|
|
105
|
-
}
|
|
108
|
+
},
|
|
109
|
+
...unavailable
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
112
|
const ours = /* @__PURE__ */ new Set();
|
|
@@ -115,7 +119,8 @@ async function fetchEnvReusingSecrets(config, options) {
|
|
|
115
119
|
keys: secretKeys,
|
|
116
120
|
revoked: revokeSuperseded ? [...ours] : [],
|
|
117
121
|
superseded: revokeSuperseded ? [] : [...ours]
|
|
118
|
-
}
|
|
122
|
+
},
|
|
123
|
+
...unavailable
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
/** Read the branch credential's secrets out of an env source. */
|
|
@@ -197,14 +202,7 @@ const apiOptions = (ctx) => ({
|
|
|
197
202
|
...ctx.apiHost ? { apiHost: ctx.apiHost } : {},
|
|
198
203
|
...ctx.api ? { api: ctx.api } : {}
|
|
199
204
|
});
|
|
200
|
-
/**
|
|
201
|
-
* Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
|
|
202
|
-
* Data API, a bucket, the AI Gateway) that the linked remote branch does not
|
|
203
|
-
* have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
|
|
204
|
-
* to "run without injection" — this is a hard stop: the user's intent (a policy)
|
|
205
|
-
* cannot be honored, and silently dropping the secret would be more confusing
|
|
206
|
-
* than refusing to start. The fix is to provision the resource first.
|
|
207
|
-
*/
|
|
205
|
+
/** Policy mismatches hard-stop because running without declared env vars violates user intent. */
|
|
208
206
|
var DevEnvMismatchError = class extends Error {
|
|
209
207
|
constructor(..._args) {
|
|
210
208
|
super(..._args);
|
|
@@ -247,12 +245,17 @@ var ServiceNotOnBranchError = class extends Error {
|
|
|
247
245
|
* 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
|
|
248
246
|
* check it against the branch's live state (`plan`); if it declares a resource
|
|
249
247
|
* the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
|
|
250
|
-
* `neonctl deploy`.
|
|
248
|
+
* `neonctl deploy`. Function declarations are excluded from that check: their
|
|
249
|
+
* invocation URLs are derived from the branch connection host, so an undeployed
|
|
250
|
+
* function still gets `NEON_FUNCTION_*_BASE_URL`. Otherwise `fetchEnv` evaluates
|
|
251
|
+
* the policy.
|
|
251
252
|
* 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
|
|
252
253
|
* branch's live state (Auth / Data API enablement plus any object-storage
|
|
253
254
|
* buckets) into a config, then `fetchEnv` resolves what is actually enabled —
|
|
254
255
|
* so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
|
|
255
|
-
*
|
|
256
|
+
* Function invocation URLs are listed live (`functionUrls: "all-live"`) because
|
|
257
|
+
* `pullConfig` cannot round-trip them into `preview.functions`. With
|
|
258
|
+
* {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
|
|
256
259
|
* `pullConfig` cannot read it back.
|
|
257
260
|
* 3. otherwise -> throw {@link MissingBranchContextError}.
|
|
258
261
|
*
|
|
@@ -261,12 +264,11 @@ var ServiceNotOnBranchError = class extends Error {
|
|
|
261
264
|
*/
|
|
262
265
|
const resolveNeonEnvVars = async (ctx) => {
|
|
263
266
|
if (ctx.services !== void 0 || ctx.envKeys !== void 0) return await resolveSelectedServices(ctx, ctx.services ?? [], ctx.envKeys ?? []);
|
|
264
|
-
const config = await loadNeonConfig(ctx
|
|
267
|
+
const config = await loadNeonConfig(ctx);
|
|
265
268
|
if (config) {
|
|
266
269
|
if (!ctx.projectId || !ctx.branchId) throw new MissingBranchContextError(`Found a neon.ts but could not resolve the project/branch. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
|
|
267
|
-
|
|
268
|
-
await
|
|
269
|
-
return await fetchAndProject(envConfig, ctx);
|
|
270
|
+
await assertPolicyMatchesBranch(withoutPreviewFunctions(config), ctx);
|
|
271
|
+
return await fetchAndProject(config, ctx);
|
|
270
272
|
}
|
|
271
273
|
if (ctx.projectId && ctx.branchId) {
|
|
272
274
|
const pulled = await pullConfig({
|
|
@@ -274,7 +276,7 @@ const resolveNeonEnvVars = async (ctx) => {
|
|
|
274
276
|
branchId: ctx.branchId,
|
|
275
277
|
...apiOptions(ctx)
|
|
276
278
|
});
|
|
277
|
-
if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx);
|
|
279
|
+
if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx, { functionUrls: "all-live" });
|
|
278
280
|
return await resolveWithImpliedGateway(pulled.config, ctx, {
|
|
279
281
|
projectId: ctx.projectId,
|
|
280
282
|
branchId: ctx.branchId
|
|
@@ -313,11 +315,12 @@ const withAiGateway = (config) => ({
|
|
|
313
315
|
*/
|
|
314
316
|
const resolveWithImpliedGateway = async (config, ctx, branch) => {
|
|
315
317
|
const unreachable = await credentialsUnreachable(ctx, branch);
|
|
316
|
-
if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx);
|
|
318
|
+
if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx, { functionUrls: "all-live" });
|
|
317
319
|
log.warning(`Could not reach the AI Gateway's credentials, so %s were not resolved. Everything else was. Either this project does not have the AI Gateway, or the call failed — \`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [NEON_ENV_VAR_KEYS.aiGateway.apiKey, NEON_ENV_VAR_KEYS.aiGateway.baseUrl].join(" and "), unreachable);
|
|
320
|
+
const pulled = await fetchAndProject(config, ctx, { functionUrls: "all-live" });
|
|
318
321
|
return {
|
|
319
|
-
...
|
|
320
|
-
skipped: ["ai-gateway"]
|
|
322
|
+
...pulled,
|
|
323
|
+
skipped: [...pulled.skipped ?? [], "ai-gateway"]
|
|
321
324
|
};
|
|
322
325
|
};
|
|
323
326
|
/**
|
|
@@ -354,12 +357,17 @@ const resolveSelectedServices = async (ctx, directServices, envKeys) => {
|
|
|
354
357
|
const api = apiFor(ctx);
|
|
355
358
|
const has = (service) => services.includes(service);
|
|
356
359
|
const checkAiGateway = has("ai-gateway") && !selectedKeys.includes(NEON_ENV_VAR_KEYS.aiGateway.apiKey);
|
|
357
|
-
const [auth, dataApiEnabled, buckets, aiGatewayAvailable] = await Promise.all([
|
|
360
|
+
const [auth, dataApiEnabled, buckets, aiGatewayAvailable, functions] = await Promise.all([
|
|
358
361
|
has("auth") ? api.getNeonAuth(projectId, branchId) : null,
|
|
359
362
|
has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
|
|
360
363
|
has("object-storage") ? api.listBranchBuckets(projectId, branchId) : null,
|
|
361
|
-
checkAiGateway ? readAiGatewayAvailable(api, projectId, branchId) : null
|
|
364
|
+
checkAiGateway ? readAiGatewayAvailable(api, projectId, branchId) : null,
|
|
365
|
+
directServices.includes("functions") ? api.listBranchFunctions(projectId, branchId) : null
|
|
362
366
|
]);
|
|
367
|
+
const callableFunctions = (functions ?? []).filter((fn) => fn.invocationUrl !== "");
|
|
368
|
+
if (directServices.includes("functions") && callableFunctions.length === 0) throw new ServiceNotOnBranchError(`--service functions: branch ${branchId} has no deployed functions, so there are no NEON_FUNCTION_*_BASE_URL vars to pull. Deploy a function first (\`${getCliName()} deploy\`, or in the Neon Console), or drop functions from --service.`);
|
|
369
|
+
const functionKeys = directServices.includes("functions") ? callableFunctions.map((fn) => functionBaseUrlKey(fn.slug)).sort((left, right) => left.localeCompare(right)) : [];
|
|
370
|
+
const fetchKeys = [.../* @__PURE__ */ new Set([...selectedKeys, ...functionKeys])];
|
|
363
371
|
const config = configForServices(services, branchId, {
|
|
364
372
|
authEnabled: auth !== null,
|
|
365
373
|
dataApiEnabled,
|
|
@@ -370,8 +378,12 @@ const resolveSelectedServices = async (ctx, directServices, envKeys) => {
|
|
|
370
378
|
envKeys
|
|
371
379
|
});
|
|
372
380
|
return await fetchAndProject(config, ctx, {
|
|
373
|
-
keys:
|
|
374
|
-
revokeSuperseded: false
|
|
381
|
+
keys: fetchKeys,
|
|
382
|
+
revokeSuperseded: false,
|
|
383
|
+
...directServices.includes("functions") ? {
|
|
384
|
+
functionUrls: "all-live",
|
|
385
|
+
listedFunctions: callableFunctions
|
|
386
|
+
} : {}
|
|
375
387
|
});
|
|
376
388
|
};
|
|
377
389
|
/**
|
|
@@ -395,7 +407,7 @@ const readAiGatewayAvailable = async (api, projectId, branchId) => {
|
|
|
395
407
|
await api.listCredentials(projectId, branchId);
|
|
396
408
|
return true;
|
|
397
409
|
} catch (error) {
|
|
398
|
-
if (isPlatformError(error) && error.code === ErrorCode$1.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
|
|
410
|
+
if (isPlatformError$1(error) && error.code === ErrorCode$1.FeatureUnavailable && (error.details.status === 404 || error.details.status === 501)) return false;
|
|
399
411
|
throw error;
|
|
400
412
|
}
|
|
401
413
|
};
|
|
@@ -483,15 +495,7 @@ const resolveDevEnv = async (ctx) => {
|
|
|
483
495
|
};
|
|
484
496
|
}
|
|
485
497
|
};
|
|
486
|
-
/**
|
|
487
|
-
* Return the policy with its `preview.functions` removed, so the env path never enumerates
|
|
488
|
-
* functions against the Neon API. Functions are local-source-bundled and produce no
|
|
489
|
-
* branch-level secrets, so they are irrelevant to env resolution; probing them only risks
|
|
490
|
-
* failing the whole resolve (undeployed function, or Functions Preview disabled on the
|
|
491
|
-
* project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
|
|
492
|
-
* they DO carry env, so they must still be checked and resolved. Returns the config
|
|
493
|
-
* unchanged when it declares no functions.
|
|
494
|
-
*/
|
|
498
|
+
/** Functions are omitted because `plan` treats undeployed functions as resources to create. */
|
|
495
499
|
const withoutPreviewFunctions = (config) => {
|
|
496
500
|
const preview = config.preview;
|
|
497
501
|
if (!preview?.functions) return config;
|
|
@@ -507,10 +511,6 @@ const withoutPreviewFunctions = (config) => {
|
|
|
507
511
|
* it declares a branch-level resource the branch is missing. Built on `plan` so
|
|
508
512
|
* it covers every present and future provisionable resource for free: any
|
|
509
513
|
* `create` action is a resource `neonctl deploy` would provision.
|
|
510
|
-
*
|
|
511
|
-
* Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
|
|
512
|
-
* `plan` probe never enumerates the functions API — an undeployed function, or a project
|
|
513
|
-
* without the Functions Preview, must never block local dev or sink env injection.
|
|
514
514
|
*/
|
|
515
515
|
const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
516
516
|
const missing = (await plan(config, {
|
|
@@ -528,14 +528,23 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
|
528
528
|
* — and functions are excluded (see {@link assertPolicyMatchesBranch}).
|
|
529
529
|
*/
|
|
530
530
|
const isMissingResource = (change) => change.kind === "service" && change.action === "create" && !change.identifier.startsWith("function:");
|
|
531
|
-
const fetchAndProject = async (config, ctx, opts = {}) =>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
531
|
+
const fetchAndProject = async (config, ctx, opts = {}) => {
|
|
532
|
+
const result = await fetchEnvReusingSecrets(config, {
|
|
533
|
+
projectId: ctx.projectId,
|
|
534
|
+
branch: ctx.branchId,
|
|
535
|
+
...apiOptions(ctx),
|
|
536
|
+
...ctx.env ? { env: ctx.env } : {},
|
|
537
|
+
...opts.keys ? { keys: opts.keys } : {},
|
|
538
|
+
...opts.revokeSuperseded === false ? { revokeSuperseded: false } : {},
|
|
539
|
+
...opts.functionUrls ? { functionUrls: opts.functionUrls } : {},
|
|
540
|
+
...opts.listedFunctions ? { listedFunctions: opts.listedFunctions } : {}
|
|
541
|
+
});
|
|
542
|
+
return {
|
|
543
|
+
vars: result.vars,
|
|
544
|
+
credential: result.credential,
|
|
545
|
+
...result.functionUrlsUnavailable ? { skipped: ["functions"] } : {}
|
|
546
|
+
};
|
|
547
|
+
};
|
|
539
548
|
/**
|
|
540
549
|
* Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
|
|
541
550
|
* root. Returns `null` when there is none (the common "no config" case), and
|
|
@@ -570,14 +579,17 @@ const looksLikeMissingDependency = (err) => {
|
|
|
570
579
|
const text = errorChainText(err);
|
|
571
580
|
return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
|
|
572
581
|
};
|
|
573
|
-
const loadNeonConfig = async (
|
|
582
|
+
const loadNeonConfig = async (ctx) => {
|
|
574
583
|
try {
|
|
575
|
-
const { config } = await loadConfigFromFile({
|
|
584
|
+
const { config } = await loadConfigFromFile({
|
|
585
|
+
cwd: ctx.cwd,
|
|
586
|
+
...ctx.omitUnsetFunctionEnv ? { unsetFunctionEnv: "omit" } : {}
|
|
587
|
+
});
|
|
576
588
|
return config;
|
|
577
589
|
} catch (err) {
|
|
578
590
|
const message = err instanceof Error ? err.message : String(err);
|
|
579
591
|
if (/Could not find a Neon config file/i.test(message)) return null;
|
|
580
|
-
if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(cwd))}\`, then try again.\nOriginal error: ${message}`);
|
|
592
|
+
if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(ctx.cwd))}\`, then try again.\nOriginal error: ${message}`);
|
|
581
593
|
throw err;
|
|
582
594
|
}
|
|
583
595
|
};
|