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.
- package/README.md +52 -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 +1 -1
- 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 +42 -5
- 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 +2 -0
- package/dist/commands/profile.js +4 -3
- package/dist/config_services.js +18 -0
- package/dist/context.js +3 -1
- package/dist/credential_io.js +1 -1
- package/dist/init/auth.js +2 -2
- package/dist/init/profile_cli.js +2 -2
- package/dist/retire_credential.js +1 -1
- package/dist/utils/middlewares.js +1 -1
- package/package.json +5 -5
|
@@ -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";
|
|
@@ -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,
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import {
|
|
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-
|
|
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-
|
|
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 };
|
package/dist/credential_io.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as storeFor } from "./_chunks/credential_io-
|
|
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
|
|
2
|
-
import { l as
|
|
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";
|
package/dist/init/profile_cli.js
CHANGED
|
@@ -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 {
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
66
|
+
"@neon/config-runtime": "1.0.3",
|
|
67
67
|
"@neon/sdk": "2.2.0",
|
|
68
|
-
"@neon/config": "1.0.
|
|
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
|
|
101
|
-
"@neon/
|
|
100
|
+
"@neon/e2e-harness": "0.0.0",
|
|
101
|
+
"@neon-internals/env-core": "0.0.3"
|
|
102
102
|
},
|
|
103
103
|
"publishConfig": {
|
|
104
104
|
"access": "public",
|