neon 4.4.0 → 4.5.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.
@@ -0,0 +1,466 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { isCi } from "../env.js";
3
+ import { log } from "../log.js";
4
+ import { applyContext, contextBranch, ensureGitignored, readContextFile } from "../context.js";
5
+ import { declaredNeonServices } from "../config_services.js";
6
+ import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
7
+ import { mergeEnvFile, resolveEnvFilePath } from "../env_file.js";
8
+ import { t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
9
+ import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
10
+ import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, removeClaimableCredentials, resolveClaimableContext, writeClaimableCredentials } from "../claimable/state.js";
11
+ import { writer } from "../writer.js";
12
+ import { noPassthrough } from "../utils/flags.js";
13
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import open from "open";
17
+ import prompts from "prompts";
18
+ import { loadConfigFromFile } from "@neon/config-runtime";
19
+ //#region src/commands/claim.ts
20
+ var claim_exports = /* @__PURE__ */ __exportAll({
21
+ CLAIM_CREATE_FIELDS: () => CLAIM_CREATE_FIELDS,
22
+ CLAIM_LIST_FIELDS: () => CLAIM_LIST_FIELDS,
23
+ aliases: () => aliases,
24
+ builder: () => builder,
25
+ claimCreateFields: () => claimCreateFields,
26
+ claimableCapabilities: () => claimableCapabilities,
27
+ command: () => command,
28
+ describe: () => describe,
29
+ findNeonConfig: () => findNeonConfig,
30
+ handler: () => handler
31
+ });
32
+ const CAPABILITY_FOR_SERVICE = {
33
+ postgres: "postgres",
34
+ auth: "auth",
35
+ "data-api": "data_api",
36
+ functions: "functions",
37
+ "object-storage": "storage",
38
+ "ai-gateway": "ai_gateway"
39
+ };
40
+ const CAPABILITY_ORDER = [
41
+ "postgres",
42
+ "data_api",
43
+ "auth",
44
+ "storage",
45
+ "functions",
46
+ "ai_gateway"
47
+ ];
48
+ const SERVICE_FOR_CAPABILITY = {
49
+ postgres: "postgres",
50
+ auth: "auth",
51
+ data_api: "data-api",
52
+ functions: "functions",
53
+ storage: "object-storage",
54
+ ai_gateway: "ai-gateway"
55
+ };
56
+ const isClaimableCapability = (value) => value in SERVICE_FOR_CAPABILITY;
57
+ const cliServiceName = (capability) => isClaimableCapability(capability) ? SERVICE_FOR_CAPABILITY[capability] : capability;
58
+ const claimableCapabilities = (services) => {
59
+ const requested = /* @__PURE__ */ new Set(["postgres"]);
60
+ for (const service of services) requested.add(CAPABILITY_FOR_SERVICE[service]);
61
+ return CAPABILITY_ORDER.filter((capability) => requested.has(capability));
62
+ };
63
+ const CONFIG_FILENAMES = [
64
+ "neon.ts",
65
+ "neon.mts",
66
+ "neon.js",
67
+ "neon.mjs"
68
+ ];
69
+ const findNeonConfig = (cwd = process.cwd()) => {
70
+ let current = resolve(cwd);
71
+ const stop = resolve(homedir());
72
+ while (true) {
73
+ for (const name of CONFIG_FILENAMES) {
74
+ const candidate = join(current, name);
75
+ if (existsSync(candidate)) return candidate;
76
+ }
77
+ if (existsSync(join(current, ".git")) || current === stop) return void 0;
78
+ const parent = dirname(current);
79
+ if (parent === current) return void 0;
80
+ current = parent;
81
+ }
82
+ };
83
+ const servicesFromConfig = async (explicitPath) => {
84
+ const path = explicitPath ?? findNeonConfig();
85
+ if (!path) return [];
86
+ const { config } = await loadConfigFromFile({ path });
87
+ return declaredNeonServices(config);
88
+ };
89
+ const removeFileIfPresent = (path) => {
90
+ try {
91
+ unlinkSync(path);
92
+ } catch (error) {
93
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
94
+ throw error;
95
+ }
96
+ };
97
+ const failureMessage = (error) => error instanceof Error ? error.message : String(error);
98
+ const CLAIM_LIST_FIELDS = [
99
+ "project_id",
100
+ "branch_id",
101
+ "state",
102
+ "project_expires_at",
103
+ "origin"
104
+ ];
105
+ const CLAIM_CREATE_FIELDS = [
106
+ "project_id",
107
+ "branch_id",
108
+ "state",
109
+ "project_expires_at",
110
+ "granted_capabilities",
111
+ "denied_capabilities",
112
+ "env_file"
113
+ ];
114
+ const claimCreateFields = (denied) => CLAIM_CREATE_FIELDS.filter((field) => field !== "denied_capabilities" || denied.length > 0);
115
+ const command = "claim";
116
+ const aliases = ["claimable"];
117
+ const describe = "Create and claim temporary Neon projects";
118
+ const builder = (argv) => argv.usage("$0 claim <sub-command> [options]").option("claimable-host", {
119
+ describe: "Claimable Neon service origin",
120
+ type: "string",
121
+ default: process.env.CLAIMABLE_NEON_HOST ?? "https://claimable.neon.tech",
122
+ hidden: true
123
+ }).command("create", "Create a temporary Neon project without an account", (y) => y.option("service", servicesOption({
124
+ key: "service",
125
+ allowed: NEON_SERVICES,
126
+ describe: "Services to request for the claimable project",
127
+ also: "Postgres is always included. Services unavailable before claim are recorded and reported."
128
+ })).option("file", {
129
+ describe: "Target dotenv file. Defaults to an existing .env, otherwise .env.local",
130
+ type: "string"
131
+ }).option("config", {
132
+ describe: "Path to neon.ts. Defaults to walking up from the current directory",
133
+ type: "string"
134
+ }).option("env-pull", {
135
+ describe: "Write the provisioned DATABASE_URL and service URLs to a dotenv file",
136
+ type: "boolean",
137
+ default: true
138
+ }).strict().check(noPassthrough("claim create")), async (args) => {
139
+ const rawServices = servicesFlagValue(args.service);
140
+ const services = rawServices ? parseServices(rawServices, {
141
+ allowed: NEON_SERVICES,
142
+ flag: "--service",
143
+ onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
144
+ }) : [];
145
+ const configuredServices = await servicesFromConfig(typeof args.config === "string" ? args.config : void 0);
146
+ await create({
147
+ ...args,
148
+ services: [.../* @__PURE__ */ new Set([...services, ...configuredServices])]
149
+ });
150
+ }).command("status [project-id]", "Show a claimable project's lifecycle and claim status", (y) => y.positional("project-id", {
151
+ describe: "Project from `neon claim list`. Defaults to the project linked in this directory",
152
+ type: "string"
153
+ }).strict().check(noPassthrough("claim status")), async (args) => await status(args)).command("accept [project-id]", "Create a claim code and open the URL where a human signs in and takes the project", (y) => y.positional("project-id", {
154
+ describe: "Project from `neon claim list`. Defaults to the project linked in this directory",
155
+ type: "string"
156
+ }).option("open", {
157
+ describe: "Open the verification URL in a browser",
158
+ type: "boolean",
159
+ default: true
160
+ }).strict().check(noPassthrough("claim accept")), async (args) => await accept(args)).command("list", "List claimable projects saved on this machine", (y) => y.strict().check(noPassthrough("claim list")), async (args) => list(args)).command("delete [project-id]", "Permanently delete an unclaimed project, or drop a local record that can no longer reach the service", (y) => y.positional("project-id", {
161
+ describe: "Project from `neon claim list`. Defaults to the project linked in this directory",
162
+ type: "string"
163
+ }).option("yes", {
164
+ alias: "y",
165
+ describe: "Skip the confirmation prompt",
166
+ type: "boolean",
167
+ default: false
168
+ }).strict().check(noPassthrough("claim delete")), async (args) => await deleteProject(args)).demandCommand(1, "Run `neon claim --help` to see the subcommands.");
169
+ const handler = (_args) => {};
170
+ const rejectExplicitAccountCredential = (props) => {
171
+ if (credentialInputs().apiKeyFlag.trim() !== "" || props.profile?.trim()) throw new Error("Claimable Neon does not use a Neon account credential. Remove --api-key or --profile.");
172
+ };
173
+ const resolveTarget = (props) => {
174
+ rejectExplicitAccountCredential(props);
175
+ const requested = props.projectId?.trim();
176
+ const context = readContextFile(props.contextFile);
177
+ const linked = resolveClaimableContext(context);
178
+ const projectId = requested || linked?.projectId;
179
+ if (projectId === void 0) throw new Error("This directory is not linked to a claimable project. Pass a project id from `neon claim list`, or run `neon claim create` first.");
180
+ const credentials = readClaimableCredentials(props.configDir, projectId);
181
+ if (credentials === null) throw new Error(`The identity assertion for ${projectId} is missing. The project cannot be managed from this machine; claim it through its existing verification URL or run \`neon link\` after it is claimed.`);
182
+ const client = new ClaimableClient(credentials.origin);
183
+ const contextMatches = linked?.projectId === projectId;
184
+ if (contextMatches && linked !== null && client.origin !== new ClaimableClient(linked.origin).origin) throw new Error("The .neon context and saved identity assertion name different Claimable Neon services. Delete .neon or the assertion file and run `neon claim create` in a new directory.");
185
+ return {
186
+ projectId,
187
+ credentials,
188
+ client,
189
+ contextMatches
190
+ };
191
+ };
192
+ const requireLiveIdentity = (credentials) => {
193
+ if (assertionHasExpired(credentials)) throw new Error(`The identity assertion for ${credentials.projectId} has expired. Run \`neon claim delete ${credentials.projectId} --yes\` to drop the local record.`);
194
+ };
195
+ const isUnusableIdentity = (error) => error instanceof ClaimableServiceError && (error.code === "invalid_grant" || error.code === "project_expired" || error.code === "not_found");
196
+ const clearLocalRecord = (props, projectId, contextMatches) => {
197
+ removeClaimableCredentials(props.configDir, projectId);
198
+ if (contextMatches && existsSync(props.contextFile)) applyContext(props.contextFile, {});
199
+ };
200
+ const create = async (props) => {
201
+ rejectExplicitAccountCredential(props);
202
+ const existing = readContextFile(props.contextFile);
203
+ 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.`);
204
+ const contextFileExisted = existsSync(props.contextFile);
205
+ const envFile = props.envPull ? resolveEnvFilePath(process.cwd(), props.file) : void 0;
206
+ const envFileExisted = envFile ? existsSync(envFile) : false;
207
+ const previousEnv = envFileExisted && envFile ? readFileSync(envFile) : void 0;
208
+ const client = new ClaimableClient(props.claimableHost);
209
+ const registration = await client.register({
210
+ capabilities: claimableCapabilities(props.services ?? []),
211
+ source: "neon_cli"
212
+ });
213
+ const stored = {
214
+ version: 1,
215
+ origin: client.origin,
216
+ registrationId: registration.registrationId,
217
+ projectId: registration.project.id,
218
+ branchId: registration.project.branchId,
219
+ identityAssertion: registration.identityAssertion,
220
+ expiresAt: registration.project.expiresAt,
221
+ assertionExpires: registration.assertionExpires
222
+ };
223
+ let localStateWritten = false;
224
+ let contextWritten = false;
225
+ let envWriteAttempted = false;
226
+ let accessToken;
227
+ try {
228
+ writeClaimableCredentials(props.configDir, stored);
229
+ localStateWritten = true;
230
+ applyContext(props.contextFile, {
231
+ projectId: registration.project.id,
232
+ branch: registration.project.branchId,
233
+ claimable: {
234
+ version: 1,
235
+ origin: client.origin
236
+ }
237
+ });
238
+ contextWritten = true;
239
+ const token = await client.exchange(registration.identityAssertion);
240
+ accessToken = token.accessToken;
241
+ const credentials = await client.credentials(registration.project.id, token.accessToken);
242
+ 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.");
243
+ if (envFile) {
244
+ const env = {
245
+ DATABASE_URL: credentials.databaseUrl,
246
+ ...credentials.services.dataApi ? { NEON_DATA_API_URL: credentials.services.dataApi.url } : {},
247
+ ...credentials.services.auth ? {
248
+ NEON_AUTH_BASE_URL: credentials.services.auth.baseUrl,
249
+ NEON_AUTH_JWKS_URL: credentials.services.auth.jwksUrl
250
+ } : {}
251
+ };
252
+ envWriteAttempted = true;
253
+ mergeEnvFile(envFile, env);
254
+ ensureGitignored(envFile);
255
+ }
256
+ const granted = registration.capabilities.filter((decision) => decision.granted).map((decision) => cliServiceName(decision.capability));
257
+ const denied = registration.capabilities.filter((decision) => !decision.granted).map((decision) => ({
258
+ capability: cliServiceName(decision.capability),
259
+ reason: decision.reason,
260
+ message: decision.message
261
+ }));
262
+ writer(props).end({
263
+ project_id: registration.project.id,
264
+ branch_id: registration.project.branchId,
265
+ state: "unclaimed",
266
+ project_expires_at: registration.project.expiresAt,
267
+ granted_capabilities: granted,
268
+ denied_capabilities: denied,
269
+ ...envFile ? { env_file: envFile } : {}
270
+ }, {
271
+ fields: claimCreateFields(denied),
272
+ renderColumns: { denied_capabilities: (row) => row.denied_capabilities.map((decision) => `${decision.capability}: ${decision.message}`).join(", ") }
273
+ });
274
+ } catch (error) {
275
+ let remoteDeleted = false;
276
+ try {
277
+ const cleanupToken = accessToken ?? (await client.exchange(registration.identityAssertion)).accessToken;
278
+ await client.deleteProject(registration.project.id, cleanupToken);
279
+ remoteDeleted = true;
280
+ } catch (cleanupError) {
281
+ log.error("Claimable project %s could not be cleaned up after create failed: %s", registration.project.id, failureMessage(cleanupError));
282
+ if (localStateWritten && contextWritten) log.error("Retry cleanup with `neon claim delete --yes` from this directory.");
283
+ }
284
+ if (remoteDeleted) try {
285
+ if (localStateWritten) removeClaimableCredentials(props.configDir, registration.project.id);
286
+ if (contextWritten) {
287
+ if (contextFileExisted) applyContext(props.contextFile, existing);
288
+ else removeFileIfPresent(props.contextFile);
289
+ }
290
+ if (envWriteAttempted && envFile) {
291
+ if (envFileExisted && previousEnv) writeFileSync(envFile, previousEnv);
292
+ else removeFileIfPresent(envFile);
293
+ }
294
+ } catch (rollbackError) {
295
+ log.error("Project cleanup succeeded, but local rollback failed: %s", failureMessage(rollbackError));
296
+ }
297
+ throw error;
298
+ }
299
+ };
300
+ const status = async (props) => {
301
+ const { projectId, credentials, client, contextMatches } = resolveTarget(props);
302
+ if (assertionHasExpired(credentials)) {
303
+ writer(props).end({
304
+ project_id: projectId,
305
+ state: "expired",
306
+ reconciled: false,
307
+ project_expires_at: credentials.expiresAt
308
+ }, { fields: [
309
+ "project_id",
310
+ "state",
311
+ "reconciled",
312
+ "project_expires_at"
313
+ ] });
314
+ return;
315
+ }
316
+ try {
317
+ const token = await client.exchange(credentials.identityAssertion);
318
+ let claimState = "unclaimed";
319
+ let reconciled = false;
320
+ let claimExpiresAt;
321
+ try {
322
+ const claim = await client.claimStatus(projectId, token.accessToken);
323
+ claimState = claim.state;
324
+ reconciled = claim.reconciled;
325
+ claimExpiresAt = claim.expiresAt;
326
+ } catch (error) {
327
+ if (!(error instanceof ClaimableServiceError) || error.code !== "not_found") throw error;
328
+ }
329
+ if (reconciled) finishClaimedContext(props, projectId, contextMatches);
330
+ writer(props).end({
331
+ project_id: projectId,
332
+ state: claimState,
333
+ reconciled,
334
+ project_expires_at: credentials.expiresAt,
335
+ ...claimExpiresAt ? { claim_expires_at: claimExpiresAt } : {}
336
+ }, { fields: [
337
+ "project_id",
338
+ "state",
339
+ "reconciled",
340
+ "project_expires_at",
341
+ "claim_expires_at"
342
+ ] });
343
+ } catch (error) {
344
+ if (error instanceof ClaimableServiceError && error.code === "project_claimed") {
345
+ finishClaimedContext(props, projectId, contextMatches);
346
+ writer(props).end({
347
+ project_id: projectId,
348
+ state: "claimed",
349
+ reconciled: true
350
+ }, { fields: [
351
+ "project_id",
352
+ "state",
353
+ "reconciled"
354
+ ] });
355
+ return;
356
+ }
357
+ if (isUnusableIdentity(error)) {
358
+ writer(props).end({
359
+ project_id: projectId,
360
+ state: "expired",
361
+ reconciled: false,
362
+ project_expires_at: credentials.expiresAt
363
+ }, { fields: [
364
+ "project_id",
365
+ "state",
366
+ "reconciled",
367
+ "project_expires_at"
368
+ ] });
369
+ return;
370
+ }
371
+ throw error;
372
+ }
373
+ };
374
+ const accept = async (props) => {
375
+ const { projectId, credentials, client } = resolveTarget(props);
376
+ requireLiveIdentity(credentials);
377
+ const token = await client.exchange(credentials.identityAssertion);
378
+ const claim = await client.createClaim(projectId, token.accessToken);
379
+ writer(props).end({
380
+ project_id: projectId,
381
+ user_code: claim.userCode,
382
+ verification_url: claim.verificationUriComplete,
383
+ expires_in_seconds: claim.expiresIn
384
+ }, { fields: [
385
+ "project_id",
386
+ "user_code",
387
+ "verification_url",
388
+ "expires_in_seconds"
389
+ ] });
390
+ if (props.open && !isCi()) open(claim.verificationUriComplete).catch(() => {
391
+ log.info("Could not open a browser. Open %s", claim.verificationUriComplete);
392
+ });
393
+ else if (props.open) log.info("Browser opening is disabled in CI. Open %s", claim.verificationUriComplete);
394
+ };
395
+ const list = (props) => {
396
+ rejectExplicitAccountCredential(props);
397
+ const projects = listClaimableCredentials(props.configDir).map((item) => {
398
+ const projectMs = Date.parse(item.expiresAt);
399
+ const projectExpired = Number.isFinite(projectMs) && projectMs <= Date.now();
400
+ return {
401
+ project_id: item.projectId,
402
+ branch_id: item.branchId,
403
+ state: assertionHasExpired(item) || projectExpired ? "expired" : "unclaimed",
404
+ project_expires_at: item.expiresAt,
405
+ origin: item.origin
406
+ };
407
+ });
408
+ writer(props).end(projects, {
409
+ fields: CLAIM_LIST_FIELDS,
410
+ emptyMessage: "No Claimable Neon projects are saved on this machine."
411
+ });
412
+ };
413
+ const deleteProject = async (props) => {
414
+ const { projectId, credentials, client, contextMatches } = resolveTarget(props);
415
+ if (!props.yes) {
416
+ if (isCi() || !process.stdin.isTTY) throw new Error("Deleting a claimable project requires confirmation. Re-run interactively or pass --yes.");
417
+ const { proceed } = await prompts({
418
+ type: "confirm",
419
+ name: "proceed",
420
+ message: `Permanently delete ${projectId}?`,
421
+ initial: false
422
+ });
423
+ if (!proceed) {
424
+ log.info("Claimable project was not deleted.");
425
+ return;
426
+ }
427
+ }
428
+ if (assertionHasExpired(credentials)) {
429
+ clearLocalRecord(props, projectId, contextMatches);
430
+ writer(props).end({
431
+ project_id: projectId,
432
+ state: "cleared"
433
+ }, { fields: ["project_id", "state"] });
434
+ return;
435
+ }
436
+ try {
437
+ const token = await client.exchange(credentials.identityAssertion);
438
+ await client.deleteProject(projectId, token.accessToken);
439
+ } catch (error) {
440
+ if (!isUnusableIdentity(error)) throw error;
441
+ clearLocalRecord(props, projectId, contextMatches);
442
+ writer(props).end({
443
+ project_id: projectId,
444
+ state: "cleared"
445
+ }, { fields: ["project_id", "state"] });
446
+ return;
447
+ }
448
+ clearLocalRecord(props, projectId, contextMatches);
449
+ writer(props).end({
450
+ project_id: projectId,
451
+ state: "deleted"
452
+ }, { fields: ["project_id", "state"] });
453
+ };
454
+ const finishClaimedContext = (props, projectId, contextMatches) => {
455
+ removeClaimableCredentials(props.configDir, projectId);
456
+ if (contextMatches && existsSync(props.contextFile)) {
457
+ const context = readContextFile(props.contextFile);
458
+ applyContext(props.contextFile, {
459
+ projectId: context.projectId,
460
+ ...contextBranch(context) ? { branch: contextBranch(context) } : {}
461
+ });
462
+ }
463
+ log.info("Dropped the local identity assertion. The next command needs `neon auth` or `neon link`.");
464
+ };
465
+ //#endregion
466
+ export { CLAIM_CREATE_FIELDS, CLAIM_LIST_FIELDS, aliases, builder, claimCreateFields, claimableCapabilities, command, describe, findNeonConfig, handler, claim_exports as t };
@@ -5,6 +5,7 @@ import { getApiClient } from "../api.js";
5
5
  import { contextBranch, readContextFile } from "../context.js";
6
6
  import { getCliName } from "../utils/cli_name.js";
7
7
  import { toNeonConfigView } from "../config_format.js";
8
+ import { declaredNeonServices } from "../config_services.js";
8
9
  import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
9
10
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView } from "../config_template.js";
10
11
  import { loadEnvFileIntoProcess } from "../env_file.js";
@@ -425,17 +426,6 @@ const applyCmd = async (props) => {
425
426
  });
426
427
  };
427
428
  /**
428
- * A static service toggle (`auth` / `dataApi` / `preview.aiGateway`) is "on" unless
429
- * explicitly disabled: `true` / `{}` / `{ enabled: true }` enable it; `false` /
430
- * `{ enabled: false }` / absent leave it off. Mirrors the runtime's `isServiceEnabled`
431
- * (which isn't exported), kept tiny and pure so it can be read straight off the policy.
432
- */
433
- const isToggleEnabled = (toggle) => {
434
- if (toggle === void 0) return false;
435
- if (typeof toggle === "boolean") return toggle;
436
- return toggle.enabled !== false;
437
- };
438
- /**
439
429
  * Human-readable list of the services a `neon.ts` policy utilizes on the branch, shown under
440
430
  * the plan/apply table. Postgres is always present (every branch has it); the rest are listed
441
431
  * only when the policy declares them. This deliberately surfaces services that produce **no**
@@ -445,13 +435,15 @@ const isToggleEnabled = (toggle) => {
445
435
  * lives in the per-branch closure), so reading it straight off `config` is accurate.
446
436
  */
447
437
  const utilizedServices = (config) => {
448
- const services = ["Postgres"];
449
- if (isToggleEnabled(config.auth)) services.push("Neon Auth");
450
- if (isToggleEnabled(config.dataApi)) services.push("Data API");
451
- if (Object.keys(config.preview?.buckets ?? {}).length > 0) services.push("Object Storage");
452
- if (Object.keys(config.preview?.functions ?? {}).length > 0) services.push("Functions");
453
- if (isToggleEnabled(config.preview?.aiGateway)) services.push("AI Gateway");
454
- return services;
438
+ const labels = {
439
+ postgres: "Postgres",
440
+ auth: "Neon Auth",
441
+ "data-api": "Data API",
442
+ "object-storage": "Object Storage",
443
+ functions: "Functions",
444
+ "ai-gateway": "AI Gateway"
445
+ };
446
+ return [labels.postgres, ...declaredNeonServices(config).map((service) => labels[service])];
455
447
  };
456
448
  /**
457
449
  * Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
@@ -9,6 +9,7 @@ import { t as config_exports } from "./config.js";
9
9
  import { t as projects_exports } from "./projects.js";
10
10
  import { t as link_exports } from "./link.js";
11
11
  import { t as checkout_exports } from "./checkout.js";
12
+ import { t as claim_exports } from "./claim.js";
12
13
  import { t as connection_string_exports } from "./connection_string.js";
13
14
  import { t as data_api_exports } from "./data_api.js";
14
15
  import { t as databases_exports } from "./databases.js";
@@ -60,6 +61,7 @@ var commands_default = [
60
61
  checkout_exports,
61
62
  link_exports,
62
63
  open_exports,
64
+ claim_exports,
63
65
  init_exports,
64
66
  mcp_exports,
65
67
  plugins_exports,
@@ -1,13 +1,14 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { _ as upsertProfile, a as canDropProfilesFile, b as writeSecretFile, 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-CVXNvU2r.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-D2vw1UOw.js";
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-B1JxWELN.js";
4
4
  import { isCi } from "../env.js";
5
5
  import { log } from "../log.js";
6
6
  import { getApiClient, isNeonApiError } from "../api.js";
7
7
  import { o as isOwnedCredentialPath, r as credentialsPath } from "../_chunks/paths-g3DRqJiD.js";
8
8
  import { setAuthContext } from "../auth_context.js";
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-D95AeZ5v.js";
9
10
  import { auth, revokeToken } from "../auth.js";
10
- import { t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
11
+ import { t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
11
12
  import { readOutgoingCredential, retirePreviousCredential, revokeTokenSet, withdrawKey } from "../retire_credential.js";
12
13
  import { authFlow, usableCredential } from "./auth.js";
13
14
  import { writer } from "../writer.js";
@@ -0,0 +1,18 @@
1
+ //#region src/config_services.ts
2
+ const isToggleEnabled = (toggle) => {
3
+ if (toggle === void 0) return false;
4
+ if (typeof toggle === "boolean") return toggle;
5
+ return toggle.enabled !== false;
6
+ };
7
+ /** Postgres is omitted because every project includes it. */
8
+ const declaredNeonServices = (config) => {
9
+ const services = [];
10
+ if (isToggleEnabled(config.auth)) services.push("auth");
11
+ if (isToggleEnabled(config.dataApi)) services.push("data-api");
12
+ if (Object.keys(config.preview?.buckets ?? {}).length > 0) services.push("object-storage");
13
+ if (Object.keys(config.preview?.functions ?? {}).length > 0) services.push("functions");
14
+ if (isToggleEnabled(config.preview?.aiGateway)) services.push("ai-gateway");
15
+ return services;
16
+ };
17
+ //#endregion
18
+ export { declaredNeonServices };
package/dist/context.js CHANGED
@@ -45,6 +45,7 @@ const isConfigInit = (args) => args._[0] === "config" && args._[1] === "init" &&
45
45
  * access has already lapsed is the main reason to remove one.
46
46
  */
47
47
  const isProfileCommand = (args) => args._[0] === "profile" || args._[0] === "profiles";
48
+ const isClaimCommand = (args) => args._[0] === "claim" || args._[0] === "claimable";
48
49
  /**
49
50
  * `neon api-keys …`, under either spelling. Exempts the group from context enrichment: how
50
51
  * far a credential reaches must come from an explicit flag, never from `.neon`.
@@ -133,6 +134,7 @@ const enrichFromContext = (args) => {
133
134
  if (isProfileCommand(args)) return;
134
135
  if (isMcpCommand(args)) return;
135
136
  if (isSkillsCommand(args) || isPluginsCommand(args)) return;
137
+ if (isClaimCommand(args)) return;
136
138
  const context = readContextFile(args.contextFile);
137
139
  if (!args.orgId) args.orgId = context.orgId;
138
140
  if (!args.projectId) args.projectId = context.projectId;
@@ -238,4 +240,4 @@ const globToRegExp = (pattern) => {
238
240
  return new RegExp(`^${source}$`);
239
241
  };
240
242
  //#endregion
241
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
243
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
@@ -1,2 +1,2 @@
1
- import { t as storeFor } from "./_chunks/credential_io-D2vw1UOw.js";
1
+ import { t as storeFor } from "./_chunks/credential_io-B1JxWELN.js";
2
2
  export { storeFor };
package/dist/init/auth.js CHANGED
@@ -1,5 +1,5 @@
1
- import { l as locationForName } from "../_chunks/profiles-CVXNvU2r.js";
2
- import { l as interpretCredentials, t as storeFor } from "../_chunks/credential_io-D2vw1UOw.js";
1
+ import { l as interpretCredentials, t as storeFor } from "../_chunks/credential_io-B1JxWELN.js";
2
+ import { l as locationForName } from "../_chunks/profiles-D95AeZ5v.js";
3
3
  import { npxNeonArgs, selectedConfigDir, selectedProfileName } from "./profile_cli.js";
4
4
  import { log } from "@clack/prompts";
5
5
  import { execa } from "execa";
@@ -1,6 +1,6 @@
1
- import { t as DEFAULT_PROFILE } from "../_chunks/profiles-CVXNvU2r.js";
2
1
  import { n as configDir } from "../_chunks/paths-g3DRqJiD.js";
3
- import { i as selectCredential, t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
2
+ import { t as DEFAULT_PROFILE } from "../_chunks/profiles-D95AeZ5v.js";
3
+ import { i as selectCredential, t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
4
4
  //#region src/init/profile_cli.ts
5
5
  /**
6
6
  * API-key selections fall back to DEFAULT because init authenticates from stored credentials.
@@ -1,4 +1,4 @@
1
- import { a as OAUTH, d as scopeOf, i as API_KEY, l as interpretCredentials, t as storeFor, u as isSameCredential } from "./_chunks/credential_io-D2vw1UOw.js";
1
+ import { a as OAUTH, d as scopeOf, i as API_KEY, l as interpretCredentials, t as storeFor, u as isSameCredential } from "./_chunks/credential_io-B1JxWELN.js";
2
2
  import { log } from "./log.js";
3
3
  import { getApiClient } from "./api.js";
4
4
  import { revokeToken } from "./auth.js";
@@ -1,4 +1,4 @@
1
- import { r as recordCredentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
1
+ import { r as recordCredentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
2
2
  //#region src/utils/middlewares.ts
3
3
  /**
4
4
  * Forwarding Yargs's default would turn ambient configuration into an explicit child override.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
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/config-runtime": "1.0.2",
66
+ "@neon/config-runtime": "1.0.3",
67
67
  "@neon/sdk": "2.2.0",
68
- "@neon/config": "1.0.2"
68
+ "@neon/config": "1.0.3"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -97,8 +97,8 @@
97
97
  "typescript": "^5.9.0",
98
98
  "vitest": "^3.0.9",
99
99
  "@neon-internals/cli-core": "0.0.0",
100
- "@neon-internals/env-core": "0.0.2",
101
- "@neon/e2e-harness": "0.0.0"
100
+ "@neon/e2e-harness": "0.0.0",
101
+ "@neon-internals/env-core": "0.0.3"
102
102
  },
103
103
  "publishConfig": {
104
104
  "access": "public",