neon 4.3.1 → 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.
- package/README.md +81 -0
- package/dist/_chunks/{auth_selection-Bx2NAgkk.js → auth_selection-ktL6uoEe.js} +1 -1
- package/dist/_chunks/{credential_io-D2vw1UOw.js → credential_io-B1JxWELN.js} +2 -1
- package/dist/_chunks/{profiles-CVXNvU2r.js → profiles-D95AeZ5v.js} +4 -39
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +5 -3
- package/dist/api.js +14 -0
- package/dist/auth_context.js +1 -0
- package/dist/claimable/api.js +242 -0
- package/dist/claimable/state.js +92 -0
- package/dist/commands/auth.js +43 -6
- package/dist/commands/bootstrap.js +1 -1
- package/dist/commands/claim.js +466 -0
- package/dist/commands/config.js +10 -18
- package/dist/commands/index.js +4 -0
- package/dist/commands/plugins.js +135 -0
- package/dist/commands/profile.js +4 -3
- package/dist/config_services.js +18 -0
- package/dist/context.js +5 -2
- package/dist/credential_io.js +1 -1
- package/dist/env.js +6 -1
- package/dist/index.js +1 -0
- package/dist/init/auth.js +2 -2
- package/dist/init/profile_cli.js +2 -2
- package/dist/plugins/plan.js +69 -0
- package/dist/plugins/run.js +123 -0
- package/dist/plugins/targets.js +64 -0
- package/dist/plugins/wizard.js +36 -0
- package/dist/retire_credential.js +1 -1
- package/dist/utils/middlewares.js +1 -1
- package/package.json +5 -5
package/dist/commands/auth.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import {
|
|
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, 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-
|
|
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";
|
|
@@ -217,7 +219,8 @@ const ensureAuth = async (props) => {
|
|
|
217
219
|
if (isProfileCommand(props)) return;
|
|
218
220
|
if (isMcpOauth(props)) return;
|
|
219
221
|
if (props._[0] === "open") return;
|
|
220
|
-
if (isSkillsCommand(props)) return;
|
|
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
|
-
...
|
|
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-
|
|
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 };
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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`
|
package/dist/commands/index.js
CHANGED
|
@@ -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";
|
|
@@ -25,6 +26,7 @@ import { t as neon_auth_exports } from "./neon_auth.js";
|
|
|
25
26
|
import { t as open_exports } from "./open.js";
|
|
26
27
|
import { t as operations_exports } from "./operations.js";
|
|
27
28
|
import { t as orgs_exports } from "./orgs.js";
|
|
29
|
+
import { t as plugins_exports } from "./plugins.js";
|
|
28
30
|
import { t as profile_exports } from "./profile.js";
|
|
29
31
|
import { t as psql_exports } from "./psql.js";
|
|
30
32
|
import { t as roles_exports } from "./roles.js";
|
|
@@ -59,8 +61,10 @@ var commands_default = [
|
|
|
59
61
|
checkout_exports,
|
|
60
62
|
link_exports,
|
|
61
63
|
open_exports,
|
|
64
|
+
claim_exports,
|
|
62
65
|
init_exports,
|
|
63
66
|
mcp_exports,
|
|
67
|
+
plugins_exports,
|
|
64
68
|
skills_exports,
|
|
65
69
|
data_api_exports,
|
|
66
70
|
functions_exports,
|