neon 4.14.3 → 4.14.5

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 CHANGED
@@ -105,16 +105,23 @@ sends:
105
105
  }
106
106
  ```
107
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.
108
+ Object Storage, Functions, and the AI Gateway passed with `--service` are sent so
109
+ demand is recorded, then reported as unavailable until the project is claimed. The CLI
110
+ does not drop them from the request.
111
+
112
+ The bundled env pull still follows `neon.ts`. A `neon.ts` that names AI Gateway,
113
+ Functions, or Object Storage fails the pull, and the create is rolled back. Use
114
+ `--config` for a different policy file (registration and that pull), or `--no-env-pull`
115
+ to skip the dotenv write.
111
116
 
112
117
  The command writes:
113
118
 
114
119
  - a `.neon` context that identifies the project and Claimable Neon service;
115
120
  - an owner-only identity assertion under the CLI config directory;
116
- - `DATABASE_URL` and any granted Auth or Data API variables to `.env` or `.env.local`
117
- (disable this with `--no-env-pull`).
121
+ - `DATABASE_URL`, `DATABASE_URL_UNPOOLED`, `NEON_BRANCH`, and Auth / Data API variables
122
+ when `neon.ts` declares them (or, with no `neon.ts`, when they are provisioned) to `.env`
123
+ or `.env.local` (disable this with `--no-env-pull`). The write is the same `env pull` used
124
+ after create.
118
125
 
119
126
  Subsequent project commands automatically exchange the assertion for a short-lived agent
120
127
  token and send API calls to Claimable Neon. The service decides which operations are
@@ -125,7 +132,7 @@ neon claim status # lifecycle and transfer status
125
132
  neon projects get <project-id> # regular CLI command, same agent token
126
133
  neon psql --role-name neondb_owner -- -c "select now()"
127
134
  neon config plan
128
- neon env pull --service postgres --service auth --service data-api
135
+ neon env pull
129
136
 
130
137
  neon claim accept # create a claim code and open the transfer URL
131
138
  neon claim delete --yes # permanently delete an unclaimed project
@@ -497,6 +504,8 @@ The human-readable summary line goes to stderr and the diff body to stdout, so `
497
504
  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
505
  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`.
499
506
 
507
+ On an unclaimed Claimable Neon project, `neon.ts` is still the source of truth when it only declares Postgres, Auth, and the Data API. A `neon.ts` that declares AI Gateway, Functions, or Object Storage fails: those cannot be used until the project is claimed. Without a `neon.ts`, a bare pull writes provisioned Postgres, Auth, and Data API. Naming AI Gateway, Functions, or Object Storage with `--service` / `--env` warns and writes nothing for them. `--config` selects the policy file, the same flag as `claim create` and `config plan`.
508
+
500
509
  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.
501
510
 
502
511
  ```bash
@@ -506,6 +515,9 @@ neon env pull
506
515
  # Pull a specific branch into a specific file
507
516
  neon env pull --branch preview --file .env.preview
508
517
 
518
+ # Use a specific neon.ts
519
+ neon env pull --config ./claimable.ts
520
+
509
521
  # Only the AI Gateway
510
522
  neon env pull --service ai-gateway
511
523
 
@@ -1,5 +1,6 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
+ import { declaredNeonServices } from "../config_services.js";
3
4
  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
5
  import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
5
6
  import { credentialScopesSatisfied } from "@neon/config/v1";
@@ -232,6 +233,134 @@ var ServiceNotOnBranchError = class extends Error {
232
233
  this.name = "ServiceNotOnBranchError";
233
234
  }
234
235
  };
236
+ const CLAIMABLE_UNSUPPORTED_SERVICES = /* @__PURE__ */ new Set([
237
+ "ai-gateway",
238
+ "functions",
239
+ "object-storage"
240
+ ]);
241
+ const CLAIMABLE_RESOLVE_ORDER = [
242
+ "postgres",
243
+ "auth",
244
+ "data-api"
245
+ ];
246
+ const emptyClaimableCredential = () => ({
247
+ issued: false,
248
+ keys: [],
249
+ revoked: [],
250
+ superseded: []
251
+ });
252
+ const isClaimableUnsupportedService = (service) => CLAIMABLE_UNSUPPORTED_SERVICES.has(service);
253
+ const dropClaimableUnsupported = (services, envKeys) => {
254
+ const skipped = [];
255
+ const keptServices = [];
256
+ for (const service of services) {
257
+ if (isClaimableUnsupportedService(service)) {
258
+ if (!skipped.includes(service)) skipped.push(service);
259
+ continue;
260
+ }
261
+ keptServices.push(service);
262
+ }
263
+ const keptKeys = [];
264
+ for (const key of envKeys) {
265
+ const service = serviceForEnvKey(key);
266
+ if (service !== null && isClaimableUnsupportedService(service)) {
267
+ if (!skipped.includes(service)) skipped.push(service);
268
+ continue;
269
+ }
270
+ keptKeys.push(key);
271
+ }
272
+ return {
273
+ services: keptServices,
274
+ envKeys: keptKeys,
275
+ skipped
276
+ };
277
+ };
278
+ const warnClaimableSkipped = (skipped) => {
279
+ if (skipped.length === 0) return;
280
+ log.warning("Skipped %s: not available on an unclaimed Claimable Neon project. Postgres, Auth, and the Data API can be pulled when they are provisioned.", skipped.join(", "));
281
+ };
282
+ /**
283
+ * Claimable Neon does not complete concurrent Management API reads.
284
+ * Resolve one allowed service at a time.
285
+ */
286
+ const resolveClaimableServicesSequentially = async (ctx, services, envKeys) => {
287
+ const leftover = envKeys.filter((key) => serviceForEnvKey(key) === null);
288
+ const parts = [];
289
+ for (const service of CLAIMABLE_RESOLVE_ORDER) {
290
+ const named = services.includes(service);
291
+ const keysForService = [...envKeys.filter((key) => serviceForEnvKey(key) === service), ...service === "postgres" ? leftover : []];
292
+ if (!named && keysForService.length === 0) continue;
293
+ parts.push(await resolveSelectedServices(ctx, named ? [service] : [], keysForService));
294
+ }
295
+ const resolvedPostgresPartition = services.includes("postgres") || envKeys.some((key) => serviceForEnvKey(key) === "postgres");
296
+ if (leftover.length > 0 && !resolvedPostgresPartition) parts.push(await resolveSelectedServices(ctx, [], leftover));
297
+ if (parts.length === 0) return {
298
+ vars: {},
299
+ credential: emptyClaimableCredential()
300
+ };
301
+ const vars = Object.assign({}, ...parts.map((part) => part.vars));
302
+ const skipped = parts.flatMap((part) => part.skipped ?? []);
303
+ return {
304
+ vars,
305
+ credential: emptyClaimableCredential(),
306
+ ...skipped.length > 0 ? { skipped } : {}
307
+ };
308
+ };
309
+ const resolveClaimableLiveEnv = async (ctx) => {
310
+ const { projectId, branchId } = ctx;
311
+ if (!projectId || !branchId) throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / \`${getCliName()} checkout\`) or pass --project-id and --branch.`);
312
+ const api = apiFor(ctx);
313
+ const auth = await api.getNeonAuth(projectId, branchId);
314
+ const dataApiEnabled = await readDataApiEnabled(api, projectId, branchId);
315
+ const services = ["postgres"];
316
+ if (auth !== null) services.push("auth");
317
+ if (dataApiEnabled !== false) services.push("data-api");
318
+ return await resolveClaimableServicesSequentially(ctx, services, []);
319
+ };
320
+ const throwPolicyMissingOnBranch = (names, branchId) => {
321
+ throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${branchId}, but the branch does not have it yet, so the matching env vars cannot be injected. Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), then re-run \`${getCliName()} dev\`.`);
322
+ };
323
+ /**
324
+ * Honor neon.ts on Claimable Neon without going through `plan` / `fetchEnv`.
325
+ * Those fire concurrent Management API reads, which Claimable does not complete.
326
+ */
327
+ const resolveClaimablePolicyEnv = async (ctx, config) => {
328
+ const declared = declaredNeonServices(config);
329
+ const unsupported = declared.filter(isClaimableUnsupportedService);
330
+ if (unsupported.length > 0) throw new DevEnvMismatchError(`Your neon.ts declares ${unsupported.join(", ")}, which cannot be used on an unclaimed Claimable Neon project. Remove those services from neon.ts.`);
331
+ const { projectId, branchId } = ctx;
332
+ if (!projectId || !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.`);
333
+ const api = apiFor(ctx);
334
+ const missing = [];
335
+ if (declared.includes("auth")) {
336
+ if (await api.getNeonAuth(projectId, branchId) === null) missing.push("auth");
337
+ }
338
+ if (declared.includes("data-api")) {
339
+ if (await readDataApiEnabled(api, projectId, branchId) === false) missing.push("data-api");
340
+ }
341
+ if (missing.length > 0) throwPolicyMissingOnBranch(missing.join(", "), branchId);
342
+ return await resolveClaimableServicesSequentially(ctx, ["postgres", ...declared], []);
343
+ };
344
+ const resolveClaimableNeonEnvVars = async (ctx) => {
345
+ if (ctx.services !== void 0 || ctx.envKeys !== void 0) {
346
+ const dropped = dropClaimableUnsupported(ctx.services ?? [], ctx.envKeys ?? []);
347
+ warnClaimableSkipped(dropped.skipped);
348
+ if (dropped.services.length === 0 && dropped.envKeys.length === 0) return {
349
+ vars: {},
350
+ credential: emptyClaimableCredential(),
351
+ ...dropped.skipped.length > 0 ? { skipped: dropped.skipped } : {}
352
+ };
353
+ const resolved = await resolveClaimableServicesSequentially(ctx, dropped.services, dropped.envKeys);
354
+ const skipped = [...resolved.skipped ?? [], ...dropped.skipped];
355
+ return {
356
+ ...resolved,
357
+ ...skipped.length > 0 ? { skipped } : {}
358
+ };
359
+ }
360
+ const config = await loadNeonConfig(ctx);
361
+ if (config) return await resolveClaimablePolicyEnv(ctx, config);
362
+ return await resolveClaimableLiveEnv(ctx);
363
+ };
235
364
  /**
236
365
  * Resolve the branch's Neon env vars (pooled / direct `DATABASE_URL`, plus Auth /
237
366
  * Data API when enabled) into a `{ KEY: value }` map. Shared by `neon dev` (which
@@ -259,10 +388,16 @@ var ServiceNotOnBranchError = class extends Error {
259
388
  * `pullConfig` cannot read it back.
260
389
  * 3. otherwise -> throw {@link MissingBranchContextError}.
261
390
  *
391
+ * {@link DevEnvContext.claimable} keeps the same precedence. Credentials are not
392
+ * minted. A neon.ts that names AI Gateway, Functions, or Object Storage fails
393
+ * instead of pulling a subset. The live and policy reads run one service at a
394
+ * time because Claimable Neon does not complete concurrent Management API reads.
395
+ *
262
396
  * Unlike {@link resolveDevEnv}, this never swallows errors — callers decide how to
263
397
  * handle them.
264
398
  */
265
399
  const resolveNeonEnvVars = async (ctx) => {
400
+ if (ctx.claimable) return await resolveClaimableNeonEnvVars(ctx);
266
401
  if (ctx.services !== void 0 || ctx.envKeys !== void 0) return await resolveSelectedServices(ctx, ctx.services ?? [], ctx.envKeys ?? []);
267
402
  const config = await loadNeonConfig(ctx);
268
403
  if (config) {
@@ -519,7 +654,7 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
519
654
  ...apiOptions(ctx)
520
655
  })).applied.filter(isMissingResource);
521
656
  if (missing.length === 0) return;
522
- throw new DevEnvMismatchError(`Your neon.ts declares ${missing.map((change) => change.identifier).join(", ")} for branch ${ctx.branchId}, but the branch does not have it yet, so the matching env vars cannot be injected. Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), then re-run \`${getCliName()} dev\`.`);
657
+ throwPolicyMissingOnBranch(missing.map((change) => change.identifier).join(", "), ctx.branchId);
523
658
  };
524
659
  /**
525
660
  * A planned change that provisions a branch-level resource the branch lacks: a
@@ -583,6 +718,7 @@ const loadNeonConfig = async (ctx) => {
583
718
  try {
584
719
  const { config } = await loadConfigFromFile({
585
720
  cwd: ctx.cwd,
721
+ ...ctx.config ? { path: ctx.config } : {},
586
722
  ...ctx.omitUnsetFunctionEnv ? { unsetFunctionEnv: "omit" } : {}
587
723
  });
588
724
  return config;
@@ -594,4 +730,4 @@ const loadNeonConfig = async (ctx) => {
594
730
  }
595
731
  };
596
732
  //#endregion
597
- export { resolveNeonEnvVars as a, resolveDevEnv as i, MissingBranchContextError as n, ServiceNotOnBranchError as r, DevEnvMismatchError as t };
733
+ export { resolveDevEnv as a, dropClaimableUnsupported as i, MissingBranchContextError as n, resolveNeonEnvVars as o, ServiceNotOnBranchError as r, DevEnvMismatchError as t };
@@ -1,4 +1,7 @@
1
1
  import { t as writeSecretFile } from "../_chunks/secure_file-BucZj4yQ.js";
2
+ import { getAuthContext } from "../auth_context.js";
3
+ import { readContextFile } from "../context.js";
4
+ import { ClaimableClient } from "./api.js";
2
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
3
6
  import { join } from "node:path";
4
7
  //#region src/claimable/state.ts
@@ -88,5 +91,21 @@ const resolveClaimableContext = (context) => {
88
91
  };
89
92
  };
90
93
  const shouldUseClaimableCredentials = (inputs, profileFlag, context) => inputs.apiKeyFlag.trim() === "" && inputs.apiKeyEnv.trim() === "" && inputs.profileEnv.trim() === "" && (profileFlag === void 0 || profileFlag.trim() === "") && resolveClaimableContext(context) !== null;
94
+ /** Management API base URL the CLI uses for a Claimable Neon origin. */
95
+ const claimableApiHost = (origin) => `${new ClaimableClient(origin).origin}/v1`;
96
+ /**
97
+ * Whether this invocation is talking to Claimable Neon, so env resolve must not
98
+ * mint credentials or pull services that require claim.
99
+ *
100
+ * Auth source wins so a same-process checkout that rewrote `.neon` without the
101
+ * marker still skips minting. A `.neon` marker plus a production API host is an
102
+ * account override and is not claimable.
103
+ */
104
+ const isClaimableEnvTarget = (props) => {
105
+ if (getAuthContext()?.source === "claimable") return true;
106
+ const linked = resolveClaimableContext(readContextFile(props.contextFile));
107
+ if (linked === null) return false;
108
+ return props.apiHost.trim().replace(/\/+$/, "") === claimableApiHost(linked.origin);
109
+ };
91
110
  //#endregion
92
- export { assertionHasExpired, claimableCredentialsPath, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials, writeClaimableCredentials };
111
+ export { assertionHasExpired, claimableApiHost, claimableCredentialsPath, isClaimableEnvTarget, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials, writeClaimableCredentials };
@@ -1,15 +1,17 @@
1
1
  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
- import { applyContext, contextBranch, ensureGitignored, readContextFile } from "../context.js";
4
+ import { getApiClient } from "../api.js";
5
+ import { applyContext, contextBranch, readContextFile } from "../context.js";
5
6
  import { claimableDataApiCreateBody, declaredNeonServices } from "../config_services.js";
6
7
  import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
7
- import { mergeEnvFile, resolveEnvFilePath } from "../env_file.js";
8
+ import { resolveEnvFilePath } from "../env_file.js";
8
9
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
9
10
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
10
11
  import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, writeClaimableCredentials } from "../claimable/state.js";
11
12
  import { writer } from "../writer.js";
12
13
  import { noPassthrough } from "../utils/flags.js";
14
+ import { pull } from "./env.js";
13
15
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
14
16
  import { dirname, join, resolve } from "node:path";
15
17
  import { homedir } from "node:os";
@@ -25,6 +27,7 @@ var claim_exports = /* @__PURE__ */ __exportAll({
25
27
  claimCreateFields: () => claimCreateFields,
26
28
  claimableCapabilities: () => claimableCapabilities,
27
29
  command: () => command,
30
+ create: () => create,
28
31
  describe: () => describe,
29
32
  findNeonConfig: () => findNeonConfig,
30
33
  handler: () => handler
@@ -84,7 +87,10 @@ const loadCreatePolicy = async (explicitPath) => {
84
87
  const path = explicitPath ?? findNeonConfig();
85
88
  if (!path) return void 0;
86
89
  const { config } = await loadConfigFromFile({ path });
87
- return config;
90
+ return {
91
+ path,
92
+ config
93
+ };
88
94
  };
89
95
  const removeFileIfPresent = (path) => {
90
96
  try {
@@ -129,10 +135,10 @@ const builder = (argv) => argv.usage("$0 claim <sub-command> [options]").option(
129
135
  describe: "Target dotenv file. Defaults to an existing .env, otherwise .env.local",
130
136
  type: "string"
131
137
  }).option("config", {
132
- describe: "Path to neon.ts. Defaults to walking up from the current directory",
138
+ describe: "Path to neon.ts for registration and the bundled env pull. Defaults to walking up from the current directory",
133
139
  type: "string"
134
140
  }).option("env-pull", {
135
- describe: "Write the provisioned DATABASE_URL and service URLs to a dotenv file",
141
+ describe: "Write the same Neon env vars `env pull` would",
136
142
  type: "boolean",
137
143
  default: true
138
144
  }).strict().check(noPassthrough("claim create")), async (args) => {
@@ -143,11 +149,12 @@ const builder = (argv) => argv.usage("$0 claim <sub-command> [options]").option(
143
149
  onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
144
150
  }) : [];
145
151
  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;
152
+ const configuredServices = policy ? declaredNeonServices(policy.config) : [];
153
+ const dataApi = policy ? claimableDataApiCreateBody(policy.config) : void 0;
148
154
  await create({
149
155
  ...args,
150
156
  services: [.../* @__PURE__ */ new Set([...services, ...configuredServices])],
157
+ ...policy ? { config: policy.path } : {},
151
158
  ...dataApi ? { dataApi } : {}
152
159
  });
153
160
  }).command("status [project-id]", "Show a claimable project's lifecycle and claim status", (y) => y.positional("project-id", {
@@ -205,7 +212,8 @@ const create = async (props) => {
205
212
  const existing = readContextFile(props.contextFile);
206
213
  if (existing.projectId || existing.orgId || existing.claimable) throw new Error(`${props.contextFile} already links this directory to a Neon project. Run \`neon claim create\` from an unlinked directory.`);
207
214
  const contextFileExisted = existsSync(props.contextFile);
208
- const envFile = props.envPull ? resolveEnvFilePath(process.cwd(), props.file) : void 0;
215
+ const cwd = props.cwd ?? process.cwd();
216
+ const envFile = props.envPull ? resolveEnvFilePath(cwd, props.file) : void 0;
209
217
  const envFileExisted = envFile ? existsSync(envFile) : false;
210
218
  const previousEnv = envFileExisted && envFile ? readFileSync(envFile) : void 0;
211
219
  const client = new ClaimableClient(props.claimableHost);
@@ -242,20 +250,26 @@ const create = async (props) => {
242
250
  contextWritten = true;
243
251
  const token = await client.exchange(registration.identityAssertion);
244
252
  accessToken = token.accessToken;
245
- const credentials = await client.credentials(registration.project.id, token.accessToken);
246
- if (credentials.projectId !== registration.project.id || credentials.branchId !== registration.project.branchId) throw new Error("Claimable Neon returned credentials for a different project. The project was not kept.");
247
253
  if (envFile) {
248
- const env = {
249
- DATABASE_URL: credentials.databaseUrl,
250
- ...credentials.services.dataApi ? { NEON_DATA_API_URL: credentials.services.dataApi.url } : {},
251
- ...credentials.services.auth ? {
252
- NEON_AUTH_BASE_URL: credentials.services.auth.baseUrl,
253
- NEON_AUTH_JWKS_URL: credentials.services.auth.jwksUrl
254
- } : {}
255
- };
254
+ const apiHost = `${client.origin}/v1`;
255
+ const apiClient = props.apiClient ?? getApiClient({
256
+ apiKey: token.accessToken,
257
+ apiHost
258
+ });
256
259
  envWriteAttempted = true;
257
- mergeEnvFile(envFile, env);
258
- ensureGitignored(envFile);
260
+ if ((await pull({
261
+ apiClient,
262
+ apiKey: token.accessToken,
263
+ apiHost,
264
+ contextFile: props.contextFile,
265
+ output: props.output,
266
+ projectId: registration.project.id,
267
+ branch: registration.project.branchId,
268
+ cwd,
269
+ file: props.file,
270
+ ...props.config ? { config: props.config } : {},
271
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
272
+ })).status === "empty") throw new Error("No Neon env variables were pulled for this claimable project.");
259
273
  }
260
274
  const granted = registration.capabilities.filter((decision) => decision.granted).map((decision) => cliServiceName(decision.capability));
261
275
  const denied = registration.capabilities.filter((decision) => !decision.granted).map((decision) => ({
@@ -467,4 +481,4 @@ const finishClaimedContext = (props, projectId, contextMatches) => {
467
481
  log.info("Dropped the local identity assertion. The next command needs `neon auth` or `neon link`.");
468
482
  };
469
483
  //#endregion
470
- export { CLAIM_CREATE_FIELDS, CLAIM_LIST_FIELDS, aliases, builder, claimCreateFields, claimableCapabilities, command, describe, findNeonConfig, handler, claim_exports as t };
484
+ export { CLAIM_CREATE_FIELDS, CLAIM_LIST_FIELDS, aliases, builder, claimCreateFields, claimableCapabilities, command, create, describe, findNeonConfig, handler, claim_exports as t };
@@ -3,9 +3,10 @@ import { log } from "../log.js";
3
3
  import { getCliName } from "../utils/cli_name.js";
4
4
  import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
5
5
  import { _ as functionBaseUrlKey } from "../_chunks/env_services-CAWrZTWa.js";
6
+ import { isClaimableEnvTarget } from "../claimable/state.js";
6
7
  import { branchIdResolve } from "../utils/enrichers.js";
7
8
  import { bundleEntry } from "../utils/esbuild.js";
8
- import { i as resolveDevEnv } from "../_chunks/env-CZmstYrY.js";
9
+ import { a as resolveDevEnv } from "../_chunks/env-DsakkOVZ.js";
9
10
  import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
10
11
  import { resolveWatchInputs } from "../dev/inputs.js";
11
12
  import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -85,7 +86,11 @@ const devEnvContext = (props, branchId, cwd) => {
85
86
  ...props.projectId ? { projectId: props.projectId } : {},
86
87
  ...branchId ? { branchId } : {},
87
88
  ...props.apiKey ? { apiKey: props.apiKey } : {},
88
- ...props.apiHost ? { apiHost: props.apiHost } : {}
89
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
90
+ ...isClaimableEnvTarget({
91
+ apiHost: props.apiHost,
92
+ contextFile: props.contextFile ?? ""
93
+ }) ? { claimable: true } : {}
89
94
  };
90
95
  };
91
96
  /**
@@ -5,10 +5,11 @@ 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
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
+ import { isClaimableEnvTarget } from "../claimable/state.js";
8
9
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
9
10
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
10
11
  import { announceTargetBranch } from "../utils/branch_notice.js";
11
- import { a as resolveNeonEnvVars } from "../_chunks/env-CZmstYrY.js";
12
+ import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-DsakkOVZ.js";
12
13
  import { existsSync } from "node:fs";
13
14
  import chalk from "chalk";
14
15
  //#region src/commands/env.ts
@@ -54,6 +55,10 @@ const builder = (argv) => argv.usage("$0 env <sub-command> [options]").options({
54
55
  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
56
  type: "array",
56
57
  string: true
58
+ },
59
+ config: {
60
+ describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
61
+ type: "string"
57
62
  }
58
63
  }).epilogue([
59
64
  "",
@@ -64,6 +69,14 @@ const builder = (argv) => argv.usage("$0 env <sub-command> [options]").options({
64
69
  " 3. Otherwise everything the branch has, plus the AI Gateway —",
65
70
  " which mints a branch credential for it.",
66
71
  "",
72
+ "On an unclaimed Claimable Neon project, neon.ts is still the source",
73
+ "of truth when it only declares Postgres, Auth, and the Data API.",
74
+ "A neon.ts that declares AI Gateway, Functions, or Object Storage",
75
+ "fails: those cannot be used until the project is claimed. Without",
76
+ "a neon.ts, a bare pull writes provisioned Postgres, Auth, and Data",
77
+ "API. Naming unsupported services with --service / --env warns",
78
+ "and writes nothing for them.",
79
+ "",
67
80
  "The pull bundled into link / checkout / config apply follows 2 and 3",
68
81
  "without the AI Gateway, so it never mints a credential you did not ask",
69
82
  "for. Run `env pull` to add it."
@@ -109,7 +122,11 @@ const NEON_OWNED_ENV_KEYS = [
109
122
  ];
110
123
  const pull = async (props, opts = {}) => {
111
124
  const cwd = props.cwd ?? process.cwd();
112
- const selectedKeys = props.services !== void 0 || props.envKeys !== void 0 ? envKeysForSelection(props.services ?? [], props.envKeys ?? []) : void 0;
125
+ const claimable = isClaimableEnvTarget(props);
126
+ const dropped = claimable && (props.services !== void 0 || props.envKeys !== void 0) ? dropClaimableUnsupported(props.services ?? [], props.envKeys ?? []) : null;
127
+ const selectionServices = dropped ? dropped.services : props.services ?? [];
128
+ const selectionEnvKeys = dropped ? dropped.envKeys : props.envKeys ?? [];
129
+ const selectedKeys = props.services !== void 0 || props.envKeys !== void 0 ? envKeysForSelection(selectionServices, selectionEnvKeys) : void 0;
113
130
  const branch = await resolveBranchRef(props);
114
131
  if (opts.announce) announceTargetBranch(props, branch, "Pulling env from branch");
115
132
  const branchId = branch.branchId;
@@ -124,20 +141,22 @@ const pull = async (props, opts = {}) => {
124
141
  ...process.env,
125
142
  ...existingEnv
126
143
  },
127
- ...props.services ? { services: props.services } : {},
128
- ...props.envKeys ? { envKeys: props.envKeys } : {},
144
+ ...props.services !== void 0 ? { services: props.services } : {},
145
+ ...props.envKeys !== void 0 ? { envKeys: props.envKeys } : {},
129
146
  ...opts.implyAiGateway ? { implyAiGateway: true } : {},
130
147
  omitUnsetFunctionEnv: true,
148
+ ...claimable ? { claimable: true } : {},
149
+ ...props.config ? { config: props.config } : {},
131
150
  ...props.apiKey ? { apiKey: props.apiKey } : {},
132
151
  ...props.apiHost ? { apiHost: props.apiHost } : {},
133
152
  ...props.runtimeApi ? { api: props.runtimeApi } : {}
134
153
  });
135
- const neonVars = pickSelectedVars(pickNeonVars(vars), selectedKeys, props.services);
154
+ const neonVars = pickSelectedVars(pickNeonVars(vars), selectedKeys, props.services !== void 0 ? selectionServices : void 0);
136
155
  if (Object.keys(neonVars).length === 0) {
137
- log.info("No Neon env variables to pull for this branch (no DATABASE_URL or enabled Auth / Data API).");
156
+ if (!(dropped !== null && dropped.services.length === 0 && dropped.envKeys.length === 0 && dropped.skipped.length > 0)) log.info("No Neon env variables to pull for this branch (no DATABASE_URL or enabled Auth / Data API).");
138
157
  return { status: "empty" };
139
158
  }
140
- const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(selectedKeys, unreachedButCurrent(skipped, existingEnv, branchId), existingEnv, props.services, props.envKeys) });
159
+ const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(selectedKeys, unreachedButCurrent(skipped, existingEnv, branchId), existingEnv, props.services !== void 0 ? selectionServices : void 0, props.envKeys !== void 0 ? selectionEnvKeys : void 0) });
141
160
  log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
142
161
  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(", "));
143
162
  if (credential?.issued) {
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-CZmstYrY.js";
2
- export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, resolveDevEnv, resolveNeonEnvVars };
1
+ import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-DsakkOVZ.js";
2
+ export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, dropClaimableUnsupported, resolveDevEnv, resolveNeonEnvVars };
package/dist/env_file.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  //#region src/env_file.ts
4
4
  /**
5
5
  * Default dotenv file `env pull` writes to: `.env` when one already exists in the working
@@ -7,7 +7,7 @@ import { join } from "node:path";
7
7
  * `vercel env pull` convention. An explicit `--file` always wins over this.
8
8
  */
9
9
  const resolveEnvFilePath = (cwd, file) => {
10
- if (file) return join(cwd, file);
10
+ if (file) return resolve(cwd, file);
11
11
  if (existsSync(join(cwd, ".env"))) return join(cwd, ".env");
12
12
  return join(cwd, ".env.local");
13
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.14.3",
3
+ "version": "4.14.5",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/sdk": "3.2.0",
67
- "@neon/config-runtime": "1.2.2",
68
- "@neon/config": "1.3.1"
66
+ "@neon/config-runtime": "1.2.3",
67
+ "@neon/sdk": "4.0.0",
68
+ "@neon/config": "1.3.2"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -97,8 +97,8 @@
97
97
  "tsx": "4.22.3",
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
+ "@neon-internals/env-core": "0.0.10",
100
101
  "@neon-internals/cli-core": "0.0.0",
101
- "@neon-internals/env-core": "0.0.9",
102
102
  "@neon/e2e-harness": "0.0.0",
103
103
  "@neon/functions": "0.9.0"
104
104
  },