neon 4.4.0 → 4.5.2

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.
@@ -1,15 +1,17 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-CVXNvU2r.js";
3
- import { a as OAUTH, i as API_KEY, l as interpretCredentials, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-D2vw1UOw.js";
2
+ import { a as OAUTH, i as API_KEY, l as interpretCredentials, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-B1JxWELN.js";
4
3
  import { isCi } from "../env.js";
5
4
  import { log } from "../log.js";
6
5
  import { getApiClient } from "../api.js";
7
6
  import { o as isOwnedCredentialPath } from "../_chunks/paths-g3DRqJiD.js";
8
7
  import { setAuthContext } from "../auth_context.js";
9
- import { isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand } from "../context.js";
8
+ import { currentContextFile, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
9
+ import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-D95AeZ5v.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { auth, refreshToken } from "../auth.js";
12
- import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
12
+ import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
13
+ import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
14
+ import { assertionHasExpired, claimableCredentialsPath, readClaimableCredentials, resolveClaimableContext, shouldUseClaimableCredentials } from "../claimable/state.js";
13
15
  import { readOutgoingCredential, retirePreviousCredential } from "../retire_credential.js";
14
16
  import { existsSync } from "node:fs";
15
17
  import { createHash } from "node:crypto";
@@ -218,6 +220,7 @@ const ensureAuth = async (props) => {
218
220
  if (isMcpOauth(props)) return;
219
221
  if (props._[0] === "open") return;
220
222
  if (isSkillsCommand(props) || isPluginsCommand(props)) return;
223
+ if (isClaimCommand(props)) return;
221
224
  const isLocalDev = props._[0] === "dev";
222
225
  const isBootstrap = props._[0] === "bootstrap";
223
226
  const isInit = props._[0] === "init";
@@ -229,8 +232,42 @@ const ensureAuth = async (props) => {
229
232
  });
230
233
  return;
231
234
  }
235
+ const inputs = credentialInputs();
236
+ const contextFile = typeof props.contextFile === "function" ? props.contextFile() : props.contextFile ?? currentContextFile();
237
+ const localContext = readContextFile(contextFile);
238
+ if (shouldUseClaimableCredentials(inputs, props.profile, localContext)) {
239
+ const linked = resolveClaimableContext(localContext);
240
+ if (linked === null) throw new Error("The linked Claimable Neon context could not be resolved.");
241
+ const stored = readClaimableCredentials(props.configDir, linked.projectId);
242
+ const path = claimableCredentialsPath(props.configDir, linked.projectId);
243
+ if (stored === null) throw new Error(`The linked project is claimable, but its identity assertion is missing from ${path}. Run \`neon claim create\` in a new directory, or \`neon link\` after claiming the project.`);
244
+ if (assertionHasExpired(stored)) throw new Error(`The identity assertion for ${linked.projectId} has expired. Run \`neon claim delete ${linked.projectId} --yes\` to drop the local record.`);
245
+ const client = new ClaimableClient(stored.origin);
246
+ if (client.origin !== new ClaimableClient(linked.origin).origin) throw new Error(`The linked .neon file and ${path} name different Claimable Neon services. Delete .neon or the assertion file and run \`neon claim create\` in a new directory.`);
247
+ let token;
248
+ try {
249
+ token = await client.exchange(stored.identityAssertion);
250
+ } catch (error) {
251
+ if (error instanceof ClaimableServiceError && error.code === "project_claimed") throw new Error("This project was claimed. Run `neon claim status` to drop the local assertion, then `neon auth` or `neon link`.");
252
+ throw error;
253
+ }
254
+ props.apiKey = token.accessToken;
255
+ props.apiHost = `${client.origin}/v1`;
256
+ props.apiClient = getApiClient({
257
+ apiKey: token.accessToken,
258
+ apiHost: props.apiHost
259
+ });
260
+ setAuthContext({
261
+ source: "claimable",
262
+ configDir: props.configDir,
263
+ credentialsPath: path
264
+ });
265
+ log.debug("Using the linked Claimable Neon project's short-lived access token");
266
+ return;
267
+ }
268
+ if (localContext.claimable !== void 0) log.warning("This directory is linked to a claimable project, but NEON_API_KEY or NEON_PROFILE is set. This command will use that account credential instead of the unclaimed project. Unset them to keep using the unclaimed project.");
232
269
  const selection = selectCredential({
233
- ...credentialInputs(),
270
+ ...inputs,
234
271
  profileFlag: props.profile
235
272
  });
236
273
  const displaced = displacedProfileWarning(selection);
@@ -2,7 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { getCliName } from "../utils/cli_name.js";
5
- import { t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
5
+ import { t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
6
6
  import { BootstrapInputError, FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, scaffoldTemplate, templateIds } from "../init/bootstrap.js";
7
7
  import { DO_NOT_SUBSTITUTE_HINT, formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
8
8
  import { existsSync } from "node:fs";
@@ -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,