@gonvex/cli 0.1.8 → 0.1.10
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 +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1011 -41
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +2 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/templates/vite-react/_gitignore +10 -0
- package/dist/templates/vite-react/gonvex/_generated/react.ts +2 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -2,14 +2,35 @@
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, readdir, stat, writeFile, copyFile } from "node:fs/promises";
|
|
5
|
+
import { chmod, mkdir, readFile, readdir, rm, stat, writeFile, copyFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
6
7
|
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { dirname, join, relative, resolve } from "node:path";
|
|
9
|
+
import { Writable } from "node:stream";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
9
11
|
const defaultRuntimeURL = "http://localhost:8080";
|
|
10
12
|
const runtimeSyncRetryMs = 5000;
|
|
11
13
|
const runtimeStateCheckMs = 2500;
|
|
12
14
|
const supportsColor = process.env.NO_COLOR === undefined && (process.env.FORCE_COLOR !== undefined || process.stdout.isTTY);
|
|
15
|
+
const defaultAccountTokenPermissions = ["projects:read", "projects:create", "projects:keys:read"];
|
|
16
|
+
const accountTokenPermissions = [
|
|
17
|
+
"projects:read",
|
|
18
|
+
"projects:create",
|
|
19
|
+
"projects:update",
|
|
20
|
+
"projects:delete",
|
|
21
|
+
"projects:keys:read",
|
|
22
|
+
"projects:members:read",
|
|
23
|
+
"projects:members:write",
|
|
24
|
+
"projects:env:read",
|
|
25
|
+
"projects:env:write",
|
|
26
|
+
"projects:*",
|
|
27
|
+
"admin:projects",
|
|
28
|
+
"tokens:read",
|
|
29
|
+
"tokens:create",
|
|
30
|
+
"tokens:revoke",
|
|
31
|
+
"tokens:*",
|
|
32
|
+
"*",
|
|
33
|
+
];
|
|
13
34
|
function ansi(code, value) {
|
|
14
35
|
return supportsColor ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
15
36
|
}
|
|
@@ -42,20 +63,110 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
42
63
|
await runEnv(argv.slice(1));
|
|
43
64
|
return;
|
|
44
65
|
}
|
|
66
|
+
if (command === "auth") {
|
|
67
|
+
await runAuth(argv.slice(1));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (command === "login") {
|
|
71
|
+
await runLogin(argv.slice(1));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (command === "logout") {
|
|
75
|
+
await runLogout(argv.slice(1));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (command === "whoami") {
|
|
79
|
+
await runWhoAmI(argv.slice(1));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (command === "token" || command === "tokens") {
|
|
83
|
+
await runToken(argv.slice(1));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (command === "project") {
|
|
87
|
+
await runProject(argv.slice(1));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
45
90
|
printHelp();
|
|
46
91
|
throw new Error(`unknown command ${command}`);
|
|
47
92
|
}
|
|
48
93
|
export async function runCreate(argv) {
|
|
49
|
-
const
|
|
94
|
+
const optionNames = ["--template", "--runtime-url", "--runtime", "--database-mode", "--origin", "--callback-path", "--signup-mode", "--owner"];
|
|
95
|
+
const target = positionalArgs(argv, optionNames)[0] ?? "my-gonvex-app";
|
|
50
96
|
const appName = basename(target);
|
|
51
97
|
const template = valueFor(argv, "--template") ?? "vite-react";
|
|
98
|
+
const runtimeURL = (valueFor(argv, "--runtime-url") ?? valueFor(argv, "--runtime") ?? defaultRuntimeURL).replace(/\/$/, "");
|
|
99
|
+
const databaseMode = valueFor(argv, "--database-mode") ?? "single";
|
|
100
|
+
if (databaseMode !== "single" && databaseMode !== "multiTenant")
|
|
101
|
+
throw new Error("--database-mode must be single or multiTenant");
|
|
102
|
+
const googleAuth = argv.includes("--google-auth");
|
|
103
|
+
const signupMode = normalizeAuthSignupMode(valueFor(argv, "--signup-mode") ?? "personal");
|
|
104
|
+
const ownerEmail = valueFor(argv, "--owner")?.trim() ?? "";
|
|
105
|
+
if (googleAuth && signupMode === "inviteOnly" && !ownerEmail) {
|
|
106
|
+
throw new Error("--owner <verified-google-email> is required when creating an invite-only Google app");
|
|
107
|
+
}
|
|
108
|
+
const shouldProvision = googleAuth || argv.includes("--provision") || Boolean(valueFor(argv, "--runtime-url") ?? valueFor(argv, "--runtime"));
|
|
52
109
|
const root = resolve(target);
|
|
53
110
|
if (existsSync(root))
|
|
54
111
|
throw new Error(`${target} already exists`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
112
|
+
let created = null;
|
|
113
|
+
let provisioningAccountToken;
|
|
114
|
+
try {
|
|
115
|
+
await copyTemplate(template, root);
|
|
116
|
+
await rewritePackageName(root, appName);
|
|
117
|
+
await rewriteGonvexConfig(root, appName, runtimeURL);
|
|
118
|
+
if (shouldProvision) {
|
|
119
|
+
provisioningAccountToken = await accountAccessTokenForRuntime(runtimeURL);
|
|
120
|
+
created = await createRuntimeProject(runtimeURL, appName, provisioningAccountToken, databaseMode);
|
|
121
|
+
await rewriteGonvexConfig(root, created.project.id, runtimeURL);
|
|
122
|
+
await writeProjectEnv(root, runtimeURL, created.project.id, created.projectKey, true);
|
|
123
|
+
if (googleAuth) {
|
|
124
|
+
const authArgs = ["add", "google", "--project", root, "--signup-mode", signupMode];
|
|
125
|
+
const callbackPath = valueFor(argv, "--callback-path");
|
|
126
|
+
if (callbackPath)
|
|
127
|
+
authArgs.push("--callback-path", callbackPath);
|
|
128
|
+
const suppliedOrigins = valuesFor(argv, "--origin");
|
|
129
|
+
const origins = suppliedOrigins.length > 0 ? suppliedOrigins : [
|
|
130
|
+
process.env.GONVEX_APP_ORIGIN ?? process.env.VITE_APP_URL ?? "http://localhost:5173",
|
|
131
|
+
];
|
|
132
|
+
for (const origin of origins)
|
|
133
|
+
authArgs.push("--origin", origin);
|
|
134
|
+
await runAuth(authArgs);
|
|
135
|
+
if (signupMode === "inviteOnly") {
|
|
136
|
+
if (databaseMode === "multiTenant") {
|
|
137
|
+
await runAuth(["tenants", "create", `${appName} workspace`, "--owner", ownerEmail, "--project", root]);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await runAuth(["memberships", "add", "--tenant", created.project.id, "--email", ownerEmail, "--role", "owner", "--project", root]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const hasProductionOrigin = origins.some((origin) => normalizeAppOrigin(origin).startsWith("https://"));
|
|
144
|
+
if (hasProductionOrigin && !argv.includes("--allow-unready")) {
|
|
145
|
+
await runAuth(["doctor", "--project", root]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
await writeEnvLocal(root, appName, runtimeURL);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (created) {
|
|
155
|
+
try {
|
|
156
|
+
await runtimeJSON(await fetch(`${runtimeURL}/dev/projects/${encodeURIComponent(created.project.id)}`, {
|
|
157
|
+
method: "DELETE",
|
|
158
|
+
headers: provisioningAccountToken
|
|
159
|
+
? accountHeaders(provisioningAccountToken)
|
|
160
|
+
: projectAuthHeaders({ runtimeURL, projectID: created.project.id, key: created.projectKey }),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
catch (cleanupError) {
|
|
164
|
+
console.warn(`[gonvex] could not roll back runtime project ${created.project.id}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await rm(root, { recursive: true, force: true });
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
59
170
|
console.log(`[gonvex] created ${target} from ${template} template`);
|
|
60
171
|
console.log(`[gonvex] next: cd ${target} && npm install && npm run dev`);
|
|
61
172
|
}
|
|
@@ -67,6 +178,205 @@ async function runInit(argv) {
|
|
|
67
178
|
await writeEnvLocal(process.cwd(), project, runtime);
|
|
68
179
|
console.log(`[gonvex] initialized ${project}`);
|
|
69
180
|
}
|
|
181
|
+
async function runLogin(argv) {
|
|
182
|
+
const runtimeURL = await accountRuntimeForArgs(argv);
|
|
183
|
+
const suppliedToken = valueFor(argv, "--token") ?? process.env.GONVEX_ACCOUNT_TOKEN;
|
|
184
|
+
let accessToken;
|
|
185
|
+
let expiresAt;
|
|
186
|
+
if (suppliedToken) {
|
|
187
|
+
accessToken = suppliedToken.trim();
|
|
188
|
+
if (!accessToken)
|
|
189
|
+
throw new Error("--token requires a non-empty personal access token");
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const positional = positionalArgs(argv, ["--runtime-url", "--runtime", "--email", "--password", "--token"]);
|
|
193
|
+
let email = valueFor(argv, "--email") ?? positional[0] ?? "";
|
|
194
|
+
let password = valueFor(argv, "--password") ?? process.env.GONVEX_PASSWORD ?? "";
|
|
195
|
+
if ((!email || !password) && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
196
|
+
throw new Error("non-interactive login requires --email and --password (or GONVEX_PASSWORD), or --token");
|
|
197
|
+
}
|
|
198
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
199
|
+
try {
|
|
200
|
+
if (!email)
|
|
201
|
+
email = (await rl.question("Email: ")).trim();
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
rl.close();
|
|
205
|
+
}
|
|
206
|
+
if (!password)
|
|
207
|
+
password = await promptHidden("Password: ");
|
|
208
|
+
if (!email || !password)
|
|
209
|
+
throw new Error("email and password are required");
|
|
210
|
+
const response = await fetch(`${runtimeURL}/dev/auth/login`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "content-type": "application/json" },
|
|
213
|
+
body: JSON.stringify({ email, password }),
|
|
214
|
+
});
|
|
215
|
+
const payload = await runtimeJSON(response);
|
|
216
|
+
accessToken = payload.session?.accessToken ?? "";
|
|
217
|
+
expiresAt = payload.session?.expiresAt;
|
|
218
|
+
if (!accessToken)
|
|
219
|
+
throw new Error("runtime login did not return an account session");
|
|
220
|
+
}
|
|
221
|
+
const identity = await fetchAccountIdentity(runtimeURL, accessToken);
|
|
222
|
+
await saveAccountProfile(runtimeURL, accessToken, identity, expiresAt);
|
|
223
|
+
console.log(`[gonvex] logged in as ${identity.account.email} to ${runtimeURL}`);
|
|
224
|
+
}
|
|
225
|
+
async function runLogout(argv) {
|
|
226
|
+
const runtimeURL = await accountRuntimeForArgs(argv);
|
|
227
|
+
const removed = await removeAccountProfile(runtimeURL);
|
|
228
|
+
if (removed)
|
|
229
|
+
console.log(`[gonvex] logged out of ${runtimeURL}`);
|
|
230
|
+
else
|
|
231
|
+
console.log(`[gonvex] no saved login for ${runtimeURL}`);
|
|
232
|
+
}
|
|
233
|
+
async function runWhoAmI(argv) {
|
|
234
|
+
const runtimeURL = await accountRuntimeForArgs(argv);
|
|
235
|
+
const accessToken = await requireAccountAccessToken(runtimeURL);
|
|
236
|
+
const identity = await fetchAccountIdentity(runtimeURL, accessToken);
|
|
237
|
+
if (argv.includes("--json")) {
|
|
238
|
+
console.log(JSON.stringify({ runtimeURL, ...identity }, null, 2));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(`${identity.account.email} (${identity.account.role})`);
|
|
242
|
+
console.log(`Runtime: ${runtimeURL}`);
|
|
243
|
+
console.log(`Authentication: ${identity.authentication}`);
|
|
244
|
+
console.log(`Permissions: ${identity.permissions.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
async function runToken(argv) {
|
|
247
|
+
const commandArgs = positionalArgs(argv, ["--runtime-url", "--runtime", "--permission", "--expires-at"]);
|
|
248
|
+
const action = commandArgs[0];
|
|
249
|
+
if (!action || action === "help" || action === "--help") {
|
|
250
|
+
printTokenHelp();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (action === "permissions") {
|
|
254
|
+
for (const permission of accountTokenPermissions)
|
|
255
|
+
console.log(permission);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const runtimeURL = await accountRuntimeForArgs(argv);
|
|
259
|
+
const accessToken = await requireAccountAccessToken(runtimeURL);
|
|
260
|
+
if (action === "list" || action === "ls") {
|
|
261
|
+
const response = await fetch(`${runtimeURL}/dev/auth/tokens`, { headers: accountHeaders(accessToken) });
|
|
262
|
+
const payload = await runtimeJSON(response);
|
|
263
|
+
const tokens = payload.tokens ?? [];
|
|
264
|
+
if (argv.includes("--json")) {
|
|
265
|
+
console.log(JSON.stringify(tokens, null, 2));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (tokens.length === 0) {
|
|
269
|
+
console.log("[gonvex] no account tokens");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const token of tokens) {
|
|
273
|
+
const state = token.revokedAt ? "revoked" : token.expiresAt && Date.parse(token.expiresAt) <= Date.now() ? "expired" : "active";
|
|
274
|
+
console.log(`${token.id}\t${token.name}\t${state}\t${token.permissions.join(",")}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (action === "create") {
|
|
279
|
+
const positional = positionalArgs(argv, ["--runtime-url", "--runtime", "--permission", "--expires-at"]);
|
|
280
|
+
const name = positional[1] ?? "CLI provisioning";
|
|
281
|
+
let permissions = valuesFor(argv, "--permission").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
282
|
+
if (argv.includes("--full"))
|
|
283
|
+
permissions = ["*"];
|
|
284
|
+
if (permissions.length === 0)
|
|
285
|
+
permissions = defaultAccountTokenPermissions;
|
|
286
|
+
const unknown = permissions.find((permission) => !accountTokenPermissions.includes(permission));
|
|
287
|
+
if (unknown)
|
|
288
|
+
throw new Error(`unknown account token permission ${unknown}; run 'gonvex token permissions' to list valid values`);
|
|
289
|
+
const response = await fetch(`${runtimeURL}/dev/auth/tokens`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: { ...accountHeaders(accessToken), "content-type": "application/json" },
|
|
292
|
+
body: JSON.stringify({ name, permissions, expiresAt: valueFor(argv, "--expires-at") }),
|
|
293
|
+
});
|
|
294
|
+
const payload = await runtimeJSON(response);
|
|
295
|
+
if (argv.includes("--json")) {
|
|
296
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.error(`[gonvex] created account token ${payload.token.id}; copy it now because it will not be shown again:`);
|
|
300
|
+
console.log(payload.accessToken);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (action === "revoke" || action === "delete" || action === "rm") {
|
|
304
|
+
const positional = positionalArgs(argv, ["--runtime-url", "--runtime"]);
|
|
305
|
+
const id = positional[1];
|
|
306
|
+
if (!id)
|
|
307
|
+
throw new Error("usage: gonvex token revoke <token-id>");
|
|
308
|
+
const response = await fetch(`${runtimeURL}/dev/auth/tokens/${encodeURIComponent(id)}`, {
|
|
309
|
+
method: "DELETE",
|
|
310
|
+
headers: accountHeaders(accessToken),
|
|
311
|
+
});
|
|
312
|
+
await runtimeJSON(response);
|
|
313
|
+
console.log(`[gonvex] revoked account token ${id}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
printTokenHelp();
|
|
317
|
+
throw new Error(`unknown token command ${action}`);
|
|
318
|
+
}
|
|
319
|
+
async function runProject(argv) {
|
|
320
|
+
const commandArgs = positionalArgs(argv, ["--runtime-url", "--runtime", "--database-mode", "--project-root"]);
|
|
321
|
+
const action = commandArgs[0];
|
|
322
|
+
if (!action || action === "help" || action === "--help") {
|
|
323
|
+
printProjectHelp();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const runtimeURL = await accountRuntimeForArgs(argv);
|
|
327
|
+
const accountToken = await accountAccessTokenForRuntime(runtimeURL);
|
|
328
|
+
if (action === "list" || action === "ls") {
|
|
329
|
+
const projects = await fetchRuntimeProjects(runtimeURL, accountToken);
|
|
330
|
+
if (argv.includes("--json")) {
|
|
331
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (projects.length === 0) {
|
|
335
|
+
console.log("[gonvex] no projects");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
for (const project of projects)
|
|
339
|
+
console.log(`${project.id}\t${project.name}\t${project.environment ?? ""}\t${project.database ?? ""}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (action === "create") {
|
|
343
|
+
const positional = positionalArgs(argv, ["--runtime-url", "--runtime", "--database-mode", "--project-root"]);
|
|
344
|
+
const projectRoot = resolve(valueFor(argv, "--project-root") ?? ".");
|
|
345
|
+
if (!existsSync(projectRoot))
|
|
346
|
+
throw new Error(`project root ${projectRoot} does not exist`);
|
|
347
|
+
const name = positional[1] ?? basename(projectRoot);
|
|
348
|
+
const databaseMode = valueFor(argv, "--database-mode") ?? "single";
|
|
349
|
+
if (databaseMode !== "single" && databaseMode !== "multiTenant")
|
|
350
|
+
throw new Error("--database-mode must be single or multiTenant");
|
|
351
|
+
const created = await createRuntimeProject(runtimeURL, name, accountToken, databaseMode);
|
|
352
|
+
await writeProjectEnv(projectRoot, runtimeURL, created.project.id, created.projectKey, true);
|
|
353
|
+
if (argv.includes("--json")) {
|
|
354
|
+
console.log(JSON.stringify(created, null, 2));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
console.log(`[gonvex] created and configured ${created.project.name} (${created.project.id})`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (action === "select" || action === "link") {
|
|
361
|
+
const positional = positionalArgs(argv, ["--runtime-url", "--runtime", "--project-root"]);
|
|
362
|
+
const id = positional[1];
|
|
363
|
+
if (!id)
|
|
364
|
+
throw new Error("usage: gonvex project select <project-id>");
|
|
365
|
+
const projectRoot = resolve(valueFor(argv, "--project-root") ?? ".");
|
|
366
|
+
if (!existsSync(projectRoot))
|
|
367
|
+
throw new Error(`project root ${projectRoot} does not exist`);
|
|
368
|
+
const projects = await fetchRuntimeProjects(runtimeURL, accountToken);
|
|
369
|
+
const project = projects.find((candidate) => candidate.id === id);
|
|
370
|
+
if (!project)
|
|
371
|
+
throw new Error(`project ${id} was not found or is not accessible`);
|
|
372
|
+
const projectKey = await fetchRuntimeProjectKey(runtimeURL, id, accountToken);
|
|
373
|
+
await writeProjectEnv(projectRoot, runtimeURL, id, projectKey, true);
|
|
374
|
+
console.log(`[gonvex] configured ${project.name} (${id})`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
printProjectHelp();
|
|
378
|
+
throw new Error(`unknown project command ${action}`);
|
|
379
|
+
}
|
|
70
380
|
async function runDev(argv) {
|
|
71
381
|
const split = argv.indexOf("--");
|
|
72
382
|
const flagArgs = split === -1 ? argv : argv.slice(0, split);
|
|
@@ -161,6 +471,36 @@ async function runEnv(argv) {
|
|
|
161
471
|
console.log(`[gonvex] saved ${parsed.name} for ${settings.projectID}`);
|
|
162
472
|
return;
|
|
163
473
|
}
|
|
474
|
+
if (action === "push" || action === "upload") {
|
|
475
|
+
const optionPath = parsedArgs.options["--file"];
|
|
476
|
+
if (optionPath && parsedArgs.positional.length > 0) {
|
|
477
|
+
throw new Error("pass the env file either as FILE or --file, not both");
|
|
478
|
+
}
|
|
479
|
+
if (parsedArgs.positional.length > 1)
|
|
480
|
+
throw new Error("usage: gonvex env push FILE");
|
|
481
|
+
const file = optionPath ?? parsedArgs.positional[0];
|
|
482
|
+
if (!file)
|
|
483
|
+
throw new Error("usage: gonvex env push FILE");
|
|
484
|
+
const envPath = resolve(projectRoot, file);
|
|
485
|
+
let content;
|
|
486
|
+
try {
|
|
487
|
+
content = await readFile(envPath, "utf8");
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
throw new Error(`could not read env file ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
491
|
+
}
|
|
492
|
+
const names = dotEnvVariableNames(content);
|
|
493
|
+
if (names.length === 0)
|
|
494
|
+
throw new Error(`env file ${file} contains no variables; refusing to replace the project env with an empty set`);
|
|
495
|
+
const credentialName = names.find((name) => ["GONVEX_PROJECT_KEY", "GONVEX_DEPLOY_KEY", "GONVEX_KEY"].includes(name));
|
|
496
|
+
if (credentialName) {
|
|
497
|
+
throw new Error(`env file ${file} contains ${credentialName}; keep CLI project credentials out of uploaded function environment variables`);
|
|
498
|
+
}
|
|
499
|
+
const count = await replaceProjectEnv(settings, content);
|
|
500
|
+
const displayPath = relative(projectRoot, envPath) || file;
|
|
501
|
+
console.log(`[gonvex] replaced project env for ${settings.projectID} with ${count} variable(s) from ${displayPath}`);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
164
504
|
if (action === "remove" || action === "rm" || action === "unset" || action === "delete") {
|
|
165
505
|
const name = parsedArgs.positional[0];
|
|
166
506
|
if (!name)
|
|
@@ -172,6 +512,373 @@ async function runEnv(argv) {
|
|
|
172
512
|
printEnvHelp();
|
|
173
513
|
throw new Error(`unknown env command ${action}`);
|
|
174
514
|
}
|
|
515
|
+
export async function runAuth(argv) {
|
|
516
|
+
const optionsWithValues = ["--project", "--runtime-url", "--project-id", "--key", "--origin", "--callback-path", "--signup-mode", "--tenant", "--email", "--owner", "--role", "--user"];
|
|
517
|
+
const positional = positionalArgs(argv, optionsWithValues);
|
|
518
|
+
const action = positional[0];
|
|
519
|
+
if (!action || action === "help") {
|
|
520
|
+
printAuthHelp();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const projectRoot = resolve(valueFor(argv, "--project") ?? ".");
|
|
524
|
+
const settings = await loadSettings(projectRoot, {
|
|
525
|
+
runtimeURL: valueFor(argv, "--runtime-url"),
|
|
526
|
+
projectID: valueFor(argv, "--project-id"),
|
|
527
|
+
key: valueFor(argv, "--key"),
|
|
528
|
+
});
|
|
529
|
+
if (!settings.key)
|
|
530
|
+
throw new Error("GONVEX_PROJECT_KEY is required for project auth commands");
|
|
531
|
+
const endpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/google`;
|
|
532
|
+
if (action === "add" || action === "enable") {
|
|
533
|
+
const provider = positional[1];
|
|
534
|
+
if (provider !== "google")
|
|
535
|
+
throw new Error("usage: gonvex auth add google [--origin URL]");
|
|
536
|
+
const callbackPath = normalizeAuthCallbackPath(valueFor(argv, "--callback-path") ?? "/");
|
|
537
|
+
const signupMode = normalizeAuthSignupMode(valueFor(argv, "--signup-mode") ?? "personal");
|
|
538
|
+
const suppliedOrigins = valuesFor(argv, "--origin");
|
|
539
|
+
const rawOrigins = suppliedOrigins.length > 0 ? suppliedOrigins : [
|
|
540
|
+
process.env.GONVEX_APP_ORIGIN ?? process.env.VITE_APP_URL ?? "http://localhost:5173",
|
|
541
|
+
];
|
|
542
|
+
const redirectUris = [...new Set(rawOrigins.map((raw) => new URL(callbackPath, `${normalizeAppOrigin(raw)}/`).toString()))];
|
|
543
|
+
let configured = null;
|
|
544
|
+
for (const redirectUri of redirectUris) {
|
|
545
|
+
configured = await runtimeJSON(await fetch(endpoint, {
|
|
546
|
+
method: "PUT",
|
|
547
|
+
headers: { ...projectAuthHeaders(settings), "content-type": "application/json" },
|
|
548
|
+
body: JSON.stringify({ redirectUri, signupMode }),
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
await saveGoogleAuthProjectConfig(projectRoot, callbackPath, redirectUris, signupMode);
|
|
552
|
+
await writeGonvexAuthModule(projectRoot, settings, callbackPath);
|
|
553
|
+
const wired = await wireViteReactGoogleAuth(projectRoot);
|
|
554
|
+
console.log(`[gonvex] enabled Google auth for ${settings.projectID}`);
|
|
555
|
+
for (const redirectUri of redirectUris)
|
|
556
|
+
console.log(`[gonvex] registered callback ${redirectUri}`);
|
|
557
|
+
console.log(`[gonvex] wrote gonvex/auth.tsx; wrap your app with GonvexAuthProvider and render GoogleSignInButton`);
|
|
558
|
+
if (wired)
|
|
559
|
+
console.log("[gonvex] wired the Vite React starter with a production-ready sign-in screen");
|
|
560
|
+
if (!configured?.ready) {
|
|
561
|
+
console.warn(`[gonvex] Google auth is registered but the runtime is not production-ready: ${(configured?.issues ?? ["runtime broker configuration is incomplete"]).join("; ")}`);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (action === "remove" || action === "disable") {
|
|
566
|
+
const provider = positional[1];
|
|
567
|
+
if (provider !== "google")
|
|
568
|
+
throw new Error("usage: gonvex auth remove google");
|
|
569
|
+
const suppliedOrigins = valuesFor(argv, "--origin");
|
|
570
|
+
if (suppliedOrigins.length > 0) {
|
|
571
|
+
const localConfig = await loadConfig(projectRoot);
|
|
572
|
+
const callbackPath = normalizeAuthCallbackPath(valueFor(argv, "--callback-path") ?? localConfig.auth?.providers?.google?.callbackPath ?? "/");
|
|
573
|
+
const redirectUris = [...new Set(suppliedOrigins.map((raw) => new URL(callbackPath, `${normalizeAppOrigin(raw)}/`).toString()))];
|
|
574
|
+
for (const redirectUri of redirectUris) {
|
|
575
|
+
const url = new URL(endpoint);
|
|
576
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
577
|
+
await runtimeJSON(await fetch(url, { method: "DELETE", headers: projectAuthHeaders(settings) }));
|
|
578
|
+
}
|
|
579
|
+
await removeGoogleAuthProjectRedirects(projectRoot, redirectUris);
|
|
580
|
+
for (const redirectUri of redirectUris)
|
|
581
|
+
console.log(`[gonvex] removed Google callback ${redirectUri}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await runtimeJSON(await fetch(endpoint, { method: "DELETE", headers: projectAuthHeaders(settings) }));
|
|
585
|
+
await setGoogleAuthProjectEnabled(projectRoot, false);
|
|
586
|
+
console.log(`[gonvex] disabled Google auth for ${settings.projectID}`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (action === "status") {
|
|
590
|
+
const configured = await runtimeJSON(await fetch(endpoint, { headers: projectAuthHeaders(settings) }));
|
|
591
|
+
if (argv.includes("--json")) {
|
|
592
|
+
console.log(JSON.stringify(configured, null, 2));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
console.log(`Google: ${configured.enabled ? "enabled" : "disabled"}`);
|
|
596
|
+
console.log(`Broker: ${configured.ready ? "ready" : "not ready"}`);
|
|
597
|
+
console.log(`Signup: ${configured.signupMode ?? "personal"}`);
|
|
598
|
+
for (const redirectUri of configured.redirectUris ?? [])
|
|
599
|
+
console.log(`Callback: ${redirectUri}`);
|
|
600
|
+
if (configured.brokerCallbackUrl)
|
|
601
|
+
console.log(`Google Cloud redirect URI: ${configured.brokerCallbackUrl}`);
|
|
602
|
+
for (const issue of configured.issues ?? [])
|
|
603
|
+
console.log(`Issue: ${issue}`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (action === "doctor") {
|
|
607
|
+
const configured = await runtimeJSON(await fetch(endpoint, { headers: projectAuthHeaders(settings) }));
|
|
608
|
+
const localConfig = await loadConfig(projectRoot);
|
|
609
|
+
const localRedirects = localConfig.auth?.providers?.google?.redirectUris ?? [];
|
|
610
|
+
const missing = localRedirects.filter((redirectUri) => !(configured.redirectUris ?? []).includes(redirectUri));
|
|
611
|
+
const untracked = (configured.redirectUris ?? []).filter((redirectUri) => !localRedirects.includes(redirectUri));
|
|
612
|
+
const issues = [...(configured.issues ?? [])];
|
|
613
|
+
if (!configured.enabled)
|
|
614
|
+
issues.push("Google provider is disabled for this project");
|
|
615
|
+
for (const redirectUri of missing)
|
|
616
|
+
issues.push(`local callback is not registered: ${redirectUri}`);
|
|
617
|
+
for (const redirectUri of untracked)
|
|
618
|
+
issues.push(`runtime callback is not tracked in gonvex.json: ${redirectUri}`);
|
|
619
|
+
if ((configured.redirectUris ?? []).length === 0)
|
|
620
|
+
issues.push("no app callback is registered");
|
|
621
|
+
if (configured.signupMode === "inviteOnly" && (configured.tenantCount ?? 0) === 0)
|
|
622
|
+
issues.push("invite-only signup needs at least one tenant scope");
|
|
623
|
+
if (configured.signupMode === "inviteOnly" && (configured.membershipCount ?? 0) + (configured.invitationCount ?? 0) === 0)
|
|
624
|
+
issues.push("invite-only signup needs at least one member or invitation");
|
|
625
|
+
if (argv.includes("--json")) {
|
|
626
|
+
console.log(JSON.stringify({ ...configured, issues, ready: configured.ready && issues.length === 0 }, null, 2));
|
|
627
|
+
}
|
|
628
|
+
else if (configured.ready && issues.length === 0) {
|
|
629
|
+
console.log(`[gonvex] Google auth is ready for production (${configured.brokerCallbackUrl})`);
|
|
630
|
+
for (const redirectUri of configured.redirectUris ?? [])
|
|
631
|
+
console.log(`Callback: ${redirectUri}`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
for (const issue of issues)
|
|
635
|
+
console.error(`[gonvex] ${issue}`);
|
|
636
|
+
}
|
|
637
|
+
if (!configured.ready || issues.length > 0)
|
|
638
|
+
throw new Error("Google auth production readiness check failed");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (action === "users" || action === "accounts") {
|
|
642
|
+
const usersEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/users`;
|
|
643
|
+
const payload = await runtimeJSON(await fetch(usersEndpoint, { headers: projectAuthHeaders(settings) }));
|
|
644
|
+
const users = payload.users ?? [];
|
|
645
|
+
if (argv.includes("--json")) {
|
|
646
|
+
console.log(JSON.stringify(users, null, 2));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (users.length === 0) {
|
|
650
|
+
console.log(`[gonvex] no app accounts for ${settings.projectID}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
for (const user of users) {
|
|
654
|
+
console.log(`${user.id}\t${user.email ?? ""}\t${user.name ?? ""}\t${user.provider}`);
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (action === "tenants" || action === "tenant") {
|
|
659
|
+
const operation = positional[1] ?? "list";
|
|
660
|
+
const tenantsEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/tenants`;
|
|
661
|
+
if (operation === "list" || operation === "ls") {
|
|
662
|
+
const payload = await runtimeJSON(await fetch(tenantsEndpoint, { headers: projectAuthHeaders(settings) }));
|
|
663
|
+
if (argv.includes("--json")) {
|
|
664
|
+
console.log(JSON.stringify(payload.tenants ?? [], null, 2));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
for (const tenant of payload.tenants ?? [])
|
|
668
|
+
console.log(`${tenant.id}\t${tenant.name}\t${tenant.memberCount} member(s)`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (operation === "create") {
|
|
672
|
+
const name = positional[2];
|
|
673
|
+
if (!name)
|
|
674
|
+
throw new Error("usage: gonvex auth tenants create <name> [--owner email]");
|
|
675
|
+
const payload = await runtimeJSON(await fetch(tenantsEndpoint, {
|
|
676
|
+
method: "POST", headers: { ...projectAuthHeaders(settings), "content-type": "application/json" },
|
|
677
|
+
body: JSON.stringify({ name, ownerEmail: valueFor(argv, "--owner") ?? "" }),
|
|
678
|
+
}));
|
|
679
|
+
console.log(`[gonvex] created tenant ${payload.tenant.name} (${payload.tenant.id})`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
throw new Error(`unknown auth tenants command ${operation}`);
|
|
683
|
+
}
|
|
684
|
+
if (action === "members" || action === "memberships") {
|
|
685
|
+
const operation = positional[1] ?? "list";
|
|
686
|
+
const tenant = valueFor(argv, "--tenant") ?? positional[2];
|
|
687
|
+
if (!tenant)
|
|
688
|
+
throw new Error("--tenant is required for membership commands");
|
|
689
|
+
const membershipEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/memberships?tenant=${encodeURIComponent(tenant)}`;
|
|
690
|
+
if (operation === "list" || operation === "ls") {
|
|
691
|
+
const payload = await runtimeJSON(await fetch(membershipEndpoint, { headers: projectAuthHeaders(settings) }));
|
|
692
|
+
if (argv.includes("--json")) {
|
|
693
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
for (const member of payload.members ?? [])
|
|
697
|
+
console.log(`${member.userId}\t${member.email}\t${member.role}\t${member.name}`);
|
|
698
|
+
for (const invitation of payload.invitations ?? [])
|
|
699
|
+
console.log(`invited\t${invitation.email}\t${invitation.role}\t${invitation.expiresAt ?? ""}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (operation === "add" || operation === "invite") {
|
|
703
|
+
const email = valueFor(argv, "--email") ?? positional[2];
|
|
704
|
+
if (!email)
|
|
705
|
+
throw new Error("--email is required when adding a membership");
|
|
706
|
+
const role = valueFor(argv, "--role") ?? "member";
|
|
707
|
+
await runtimeJSON(await fetch(membershipEndpoint, {
|
|
708
|
+
method: "PUT", headers: { ...projectAuthHeaders(settings), "content-type": "application/json" },
|
|
709
|
+
body: JSON.stringify({ email, role }),
|
|
710
|
+
}));
|
|
711
|
+
console.log(`[gonvex] granted ${role} access to ${email} for tenant ${tenant}`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (operation === "remove" || operation === "rm") {
|
|
715
|
+
const user = valueFor(argv, "--user");
|
|
716
|
+
const email = valueFor(argv, "--email");
|
|
717
|
+
if (!user && !email)
|
|
718
|
+
throw new Error("--user is required to remove a member, or --email to revoke an invitation");
|
|
719
|
+
const target = user ? `user=${encodeURIComponent(user)}` : `email=${encodeURIComponent(email)}`;
|
|
720
|
+
await runtimeJSON(await fetch(`${membershipEndpoint}&${target}`, {
|
|
721
|
+
method: "DELETE", headers: projectAuthHeaders(settings),
|
|
722
|
+
}));
|
|
723
|
+
console.log(user
|
|
724
|
+
? `[gonvex] removed ${user} from tenant ${tenant}`
|
|
725
|
+
: `[gonvex] revoked the invitation for ${email} from tenant ${tenant}`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
throw new Error(`unknown auth memberships command ${operation}`);
|
|
729
|
+
}
|
|
730
|
+
if (action === "user") {
|
|
731
|
+
const operation = positional[1];
|
|
732
|
+
const user = valueFor(argv, "--user") ?? positional[2];
|
|
733
|
+
if (!operation || !user)
|
|
734
|
+
throw new Error("usage: gonvex auth user <disable|enable|delete> <user-id>");
|
|
735
|
+
const userEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/users/${encodeURIComponent(user)}`;
|
|
736
|
+
if (operation === "disable" || operation === "enable") {
|
|
737
|
+
await runtimeJSON(await fetch(userEndpoint, {
|
|
738
|
+
method: "PATCH", headers: { ...projectAuthHeaders(settings), "content-type": "application/json" },
|
|
739
|
+
body: JSON.stringify({ disabled: operation === "disable" }),
|
|
740
|
+
}));
|
|
741
|
+
console.log(`[gonvex] ${operation}d app account ${user}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (operation === "delete" || operation === "remove") {
|
|
745
|
+
await runtimeJSON(await fetch(userEndpoint, { method: "DELETE", headers: projectAuthHeaders(settings) }));
|
|
746
|
+
console.log(`[gonvex] deleted app account ${user}`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
throw new Error(`unknown auth user command ${operation}`);
|
|
750
|
+
}
|
|
751
|
+
printAuthHelp();
|
|
752
|
+
throw new Error(`unknown auth command ${action}`);
|
|
753
|
+
}
|
|
754
|
+
function normalizeAppOrigin(raw) {
|
|
755
|
+
let parsed;
|
|
756
|
+
try {
|
|
757
|
+
parsed = new URL(raw);
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
throw new Error(`invalid app origin ${raw}`);
|
|
761
|
+
}
|
|
762
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
763
|
+
throw new Error("--origin must contain only scheme, host, and optional port");
|
|
764
|
+
}
|
|
765
|
+
const local = ["localhost", "127.0.0.1", "[::1]", "::1"].includes(parsed.hostname);
|
|
766
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
|
|
767
|
+
throw new Error("--origin must use https (http is allowed only for localhost)");
|
|
768
|
+
}
|
|
769
|
+
return parsed.origin;
|
|
770
|
+
}
|
|
771
|
+
function normalizeAuthCallbackPath(raw) {
|
|
772
|
+
const value = raw.trim();
|
|
773
|
+
if (!value.startsWith("/") || value.startsWith("//") || value.includes("?") || value.includes("#")) {
|
|
774
|
+
throw new Error("--callback-path must be an absolute pathname without a query or fragment");
|
|
775
|
+
}
|
|
776
|
+
return value;
|
|
777
|
+
}
|
|
778
|
+
function normalizeAuthSignupMode(raw) {
|
|
779
|
+
if (raw === "personal")
|
|
780
|
+
return raw;
|
|
781
|
+
if (raw === "inviteOnly" || raw === "invite-only" || raw === "invited")
|
|
782
|
+
return "inviteOnly";
|
|
783
|
+
throw new Error("--signup-mode must be personal or inviteOnly");
|
|
784
|
+
}
|
|
785
|
+
async function saveGoogleAuthProjectConfig(root, callbackPath, addedRedirectUris, signupMode) {
|
|
786
|
+
const configPath = join(root, "gonvex.json");
|
|
787
|
+
const config = await loadConfig(root);
|
|
788
|
+
const existing = config.auth?.providers?.google;
|
|
789
|
+
const redirectUris = [...new Set([...(existing?.redirectUris ?? []), ...addedRedirectUris])].sort();
|
|
790
|
+
config.auth = {
|
|
791
|
+
...(config.auth ?? {}),
|
|
792
|
+
providers: {
|
|
793
|
+
...(config.auth?.providers ?? {}),
|
|
794
|
+
google: { ...(existing ?? {}), enabled: true, callbackPath, redirectUris, signupMode },
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
798
|
+
}
|
|
799
|
+
async function setGoogleAuthProjectEnabled(root, enabled) {
|
|
800
|
+
const configPath = join(root, "gonvex.json");
|
|
801
|
+
const config = await loadConfig(root);
|
|
802
|
+
const existing = config.auth?.providers?.google ?? {};
|
|
803
|
+
config.auth = {
|
|
804
|
+
...(config.auth ?? {}),
|
|
805
|
+
providers: { ...(config.auth?.providers ?? {}), google: { ...existing, enabled } },
|
|
806
|
+
};
|
|
807
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
808
|
+
}
|
|
809
|
+
async function removeGoogleAuthProjectRedirects(root, removedRedirectUris) {
|
|
810
|
+
const configPath = join(root, "gonvex.json");
|
|
811
|
+
const config = await loadConfig(root);
|
|
812
|
+
const existing = config.auth?.providers?.google ?? {};
|
|
813
|
+
const removed = new Set(removedRedirectUris);
|
|
814
|
+
config.auth = {
|
|
815
|
+
...(config.auth ?? {}),
|
|
816
|
+
providers: {
|
|
817
|
+
...(config.auth?.providers ?? {}),
|
|
818
|
+
google: { ...existing, redirectUris: (existing.redirectUris ?? []).filter((redirectUri) => !removed.has(redirectUri)) },
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
822
|
+
}
|
|
823
|
+
async function writeGonvexAuthModule(root, settings, callbackPath) {
|
|
824
|
+
const source = [
|
|
825
|
+
"// Generated by `gonvex auth add google`. Re-running the command replaces this file.",
|
|
826
|
+
'import { createGonvexAuth } from "./_generated/react";',
|
|
827
|
+
"",
|
|
828
|
+
"const env = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env ?? {};",
|
|
829
|
+
"",
|
|
830
|
+
"export const gonvexAuth = createGonvexAuth({",
|
|
831
|
+
` runtimeUrl: env.VITE_GONVEX_URL ?? env.VITE_GONVEX_RUNTIME_URL ?? ${JSON.stringify(settings.runtimeURL)},`,
|
|
832
|
+
` projectId: env.VITE_GONVEX_PROJECT_ID ?? ${JSON.stringify(settings.projectID)},`,
|
|
833
|
+
` callbackPath: ${JSON.stringify(callbackPath)},`,
|
|
834
|
+
"});",
|
|
835
|
+
"",
|
|
836
|
+
"export const GonvexAuthProvider = gonvexAuth.GonvexAuthProvider;",
|
|
837
|
+
"export const GoogleSignInButton = gonvexAuth.GoogleSignInButton;",
|
|
838
|
+
"export const useGonvexAuth = gonvexAuth.useGonvexAuth;",
|
|
839
|
+
"",
|
|
840
|
+
].join("\n");
|
|
841
|
+
await writeFileIfChanged(join(root, "gonvex", "auth.tsx"), source);
|
|
842
|
+
}
|
|
843
|
+
async function wireViteReactGoogleAuth(root) {
|
|
844
|
+
const mainPath = join(root, "src", "main.tsx");
|
|
845
|
+
const appPath = join(root, "src", "App.tsx");
|
|
846
|
+
if (!existsSync(mainPath) || !existsSync(appPath))
|
|
847
|
+
return false;
|
|
848
|
+
let main = await readFile(mainPath, "utf8");
|
|
849
|
+
let app = await readFile(appPath, "utf8");
|
|
850
|
+
if (main.includes('from "../gonvex/auth"') && app.includes("GoogleSignInButton"))
|
|
851
|
+
return true;
|
|
852
|
+
const providerImport = 'import { GonvexProvider } from "../gonvex/_generated/react";';
|
|
853
|
+
const reactImport = 'import { useMutation, useQuery } from "../gonvex/_generated/react";';
|
|
854
|
+
const appStart = 'export default function App(props: { runtimeURL: string }) {\n const messages = useQuery<Message[]>(api["messages.list"], {}) ?? [];';
|
|
855
|
+
if (!main.includes(providerImport) || !main.includes("<GonvexProvider client={gonvex}>") || !app.includes(reactImport) || !app.includes(appStart)) {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
main = main
|
|
859
|
+
.replace(providerImport, 'import { GonvexAuthProvider } from "../gonvex/auth";')
|
|
860
|
+
.replace("<GonvexProvider client={gonvex}>", "<GonvexAuthProvider client={gonvex}>")
|
|
861
|
+
.replace("</GonvexProvider>", "</GonvexAuthProvider>");
|
|
862
|
+
app = app
|
|
863
|
+
.replace(reactImport, `${reactImport}\nimport { GoogleSignInButton, useGonvexAuth } from "../gonvex/auth";`)
|
|
864
|
+
.replace(appStart, [
|
|
865
|
+
"export default function App(props: { runtimeURL: string }) {",
|
|
866
|
+
" const auth = useGonvexAuth();",
|
|
867
|
+
" if (!auth.isAuthenticated) {",
|
|
868
|
+
' return <main className="shell"><section className="hero"><div className="status">Secure Gonvex account</div><h1>Sign in to continue.</h1><p>Your app uses Gonvex-native Google authentication—no Firebase SDK or per-app Google Cloud project.</p><GoogleSignInButton />{auth.error ? <p role="alert">{auth.error}</p> : null}</section></main>;',
|
|
869
|
+
" }",
|
|
870
|
+
" return <AuthenticatedApp runtimeURL={props.runtimeURL} />;",
|
|
871
|
+
"}",
|
|
872
|
+
"",
|
|
873
|
+
"function AuthenticatedApp(props: { runtimeURL: string }) {",
|
|
874
|
+
" const auth = useGonvexAuth();",
|
|
875
|
+
' const messages = useQuery<Message[]>(api["messages.list"], {}) ?? [];',
|
|
876
|
+
].join("\n"))
|
|
877
|
+
.replace('<div className="status">Connected to {props.runtimeURL}</div>', '<div className="status"><span>Connected to {props.runtimeURL} as {auth.user?.email}</span><GoogleSignInButton /></div>');
|
|
878
|
+
await writeFile(mainPath, main);
|
|
879
|
+
await writeFile(appPath, app);
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
175
882
|
async function watchProject(root, settings, once, signal, initialState) {
|
|
176
883
|
const backendDir = join(root, "gonvex");
|
|
177
884
|
await mkdir(backendDir, { recursive: true });
|
|
@@ -292,7 +999,7 @@ function sanitizeProjectID(projectID) {
|
|
|
292
999
|
}
|
|
293
1000
|
async function parseRegistrations(root, file) {
|
|
294
1001
|
const source = await readFile(file, "utf8");
|
|
295
|
-
const pattern = /app\.(Query|Mutation|Action|HTTP|InternalMutation|LiveGrid)\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1002
|
+
const pattern = /app\.(Query|Mutation|Action|HTTP|PublicHTTP|InternalMutation|LiveGrid)\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
296
1003
|
const entries = {};
|
|
297
1004
|
for (const match of source.matchAll(pattern)) {
|
|
298
1005
|
entries[match[2]] = {
|
|
@@ -348,7 +1055,7 @@ async function writeBindings(root, manifest) {
|
|
|
348
1055
|
const outputs = {
|
|
349
1056
|
"api.ts": renderAPI(manifest),
|
|
350
1057
|
"client.ts": '// Generated by gonvex dev. Do not edit.\nexport { GonvexClient, ConvexReactClient } from "@gonvex/client";\n',
|
|
351
|
-
"react.ts": '// Generated by gonvex dev. Do not edit.\nexport { ConvexProvider, ConvexProviderWithAuth, ConvexReactClient, GonvexProvider, useAction, useConvex, useConvexAuth, useConvexConnectionState, useMutation, usePaginatedQuery, useQuery } from "@gonvex/react";\n',
|
|
1058
|
+
"react.ts": '// Generated by gonvex dev. Do not edit.\nexport { ConvexProvider, ConvexProviderWithAuth, ConvexReactClient, createGonvexAuth, GonvexAuthProvider, GonvexGoogleAuthButton, GonvexProvider, useAction, useConvex, useConvexAuth, useConvexConnectionState, useGonvexAuth, useMutation, usePaginatedQuery, useQuery } from "@gonvex/react";\nexport type { GonvexAuthConfig, GonvexAuthTenant, GonvexAuthUser, GonvexAuthValue } from "@gonvex/react";\n',
|
|
352
1059
|
"types.ts": "// Generated by gonvex dev. Do not edit.\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n",
|
|
353
1060
|
"schema.ts": renderSchemaIndex(manifest),
|
|
354
1061
|
"landlord/schema.ts": renderScopedSchemaModule("landlord", manifest.schema.landlordTables),
|
|
@@ -590,6 +1297,20 @@ async function saveProjectEnv(settings, name, value) {
|
|
|
590
1297
|
if (!response.ok)
|
|
591
1298
|
throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
|
|
592
1299
|
}
|
|
1300
|
+
async function replaceProjectEnv(settings, content) {
|
|
1301
|
+
const response = await fetch(projectEnvURL(settings), {
|
|
1302
|
+
method: "PUT",
|
|
1303
|
+
headers: {
|
|
1304
|
+
"content-type": "application/json",
|
|
1305
|
+
...projectAuthHeaders(settings),
|
|
1306
|
+
},
|
|
1307
|
+
body: JSON.stringify({ content }),
|
|
1308
|
+
});
|
|
1309
|
+
if (!response.ok)
|
|
1310
|
+
throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
|
|
1311
|
+
const payload = await response.json();
|
|
1312
|
+
return typeof payload.count === "number" ? payload.count : 0;
|
|
1313
|
+
}
|
|
593
1314
|
async function deleteProjectEnv(settings, name) {
|
|
594
1315
|
const response = await fetch(projectEnvURL(settings), {
|
|
595
1316
|
method: "DELETE",
|
|
@@ -621,13 +1342,30 @@ function parseEnvSetArgs(positional) {
|
|
|
621
1342
|
throw new Error("usage: gonvex env set NAME VALUE");
|
|
622
1343
|
return { name: first, value };
|
|
623
1344
|
}
|
|
1345
|
+
function dotEnvVariableNames(content) {
|
|
1346
|
+
const names = new Set();
|
|
1347
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1348
|
+
let line = rawLine.trim();
|
|
1349
|
+
if (!line || line.startsWith("#"))
|
|
1350
|
+
continue;
|
|
1351
|
+
if (line.startsWith("export "))
|
|
1352
|
+
line = line.slice("export ".length).trimStart();
|
|
1353
|
+
const equalsIndex = line.indexOf("=");
|
|
1354
|
+
if (equalsIndex < 1)
|
|
1355
|
+
continue;
|
|
1356
|
+
const name = line.slice(0, equalsIndex).trim();
|
|
1357
|
+
if (name)
|
|
1358
|
+
names.add(name);
|
|
1359
|
+
}
|
|
1360
|
+
return [...names];
|
|
1361
|
+
}
|
|
624
1362
|
function parseEnvCommandArgs(argv) {
|
|
625
1363
|
const options = {};
|
|
626
1364
|
const positional = [];
|
|
627
1365
|
let action = "";
|
|
628
1366
|
for (let index = 0; index < argv.length; index += 1) {
|
|
629
1367
|
const arg = argv[index];
|
|
630
|
-
if (["--project", "--runtime-url", "--project-id", "--key"].includes(arg)) {
|
|
1368
|
+
if (["--project", "--runtime-url", "--project-id", "--key", "--file"].includes(arg)) {
|
|
631
1369
|
const value = argv[index + 1];
|
|
632
1370
|
if (value === undefined)
|
|
633
1371
|
throw new Error(`${arg} requires a value`);
|
|
@@ -666,11 +1404,18 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
666
1404
|
return settings;
|
|
667
1405
|
if (options.keyWasExplicit)
|
|
668
1406
|
return settings;
|
|
669
|
-
|
|
1407
|
+
let accountToken = await accountAccessTokenForRuntime(settings.runtimeURL);
|
|
1408
|
+
const configuredProject = await findRuntimeProject(settings.runtimeURL, settings.projectID, accountToken).catch(() => null);
|
|
670
1409
|
if (configuredProject) {
|
|
1410
|
+
if (accountToken) {
|
|
1411
|
+
const projectKey = await fetchRuntimeProjectKey(settings.runtimeURL, configuredProject.id, accountToken);
|
|
1412
|
+
await writeProjectEnv(root, settings.runtimeURL, configuredProject.id, projectKey);
|
|
1413
|
+
console.log(`[gonvex] configured ${configuredProject.id} in .env.local`);
|
|
1414
|
+
return { ...settings, projectID: configuredProject.id, key: projectKey };
|
|
1415
|
+
}
|
|
671
1416
|
await writeProjectEnv(root, settings.runtimeURL, configuredProject.id, settings.key);
|
|
672
1417
|
console.log(`[gonvex] configured ${configuredProject.id} in .env.local`);
|
|
673
|
-
console.warn("[gonvex] GONVEX_PROJECT_KEY is not configured;
|
|
1418
|
+
console.warn("[gonvex] GONVEX_PROJECT_KEY is not configured; run 'gonvex login' or provide the project key.");
|
|
674
1419
|
return { ...settings, projectID: configuredProject.id };
|
|
675
1420
|
}
|
|
676
1421
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -682,7 +1427,8 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
682
1427
|
console.log("[gonvex] No Gonvex project is configured for this app.");
|
|
683
1428
|
const runtimeURL = await promptDefault(rl, "Runtime URL", settings.runtimeURL || defaultRuntimeURL);
|
|
684
1429
|
settings = { ...settings, runtimeURL };
|
|
685
|
-
|
|
1430
|
+
accountToken = await accountAccessTokenForRuntime(runtimeURL);
|
|
1431
|
+
const projects = await fetchRuntimeProjects(runtimeURL, accountToken);
|
|
686
1432
|
let project;
|
|
687
1433
|
if (projects.length > 0) {
|
|
688
1434
|
console.log("[gonvex] Choose a project:");
|
|
@@ -694,7 +1440,9 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
694
1440
|
const index = Number.parseInt(choice, 10);
|
|
695
1441
|
if (Number.isFinite(index) && index >= 1 && index <= projects.length) {
|
|
696
1442
|
project = projects[index - 1];
|
|
697
|
-
const projectKey =
|
|
1443
|
+
const projectKey = accountToken
|
|
1444
|
+
? await fetchRuntimeProjectKey(runtimeURL, project.id, accountToken)
|
|
1445
|
+
: await promptDefault(rl, "Project key", "");
|
|
698
1446
|
if (!projectKey)
|
|
699
1447
|
throw new Error("Gonvex project key is required for existing projects");
|
|
700
1448
|
const next = { ...settings, projectID: project.id, key: projectKey };
|
|
@@ -703,7 +1451,7 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
703
1451
|
return next;
|
|
704
1452
|
}
|
|
705
1453
|
else {
|
|
706
|
-
const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)));
|
|
1454
|
+
const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)), accountToken);
|
|
707
1455
|
project = created.project;
|
|
708
1456
|
const next = { ...settings, projectID: project.id, key: created.projectKey };
|
|
709
1457
|
await writeProjectEnv(root, runtimeURL, project.id, created.projectKey);
|
|
@@ -712,7 +1460,7 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
712
1460
|
}
|
|
713
1461
|
}
|
|
714
1462
|
else {
|
|
715
|
-
const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)));
|
|
1463
|
+
const created = await createRuntimeProject(runtimeURL, await promptDefault(rl, "New project name", basename(root)), accountToken);
|
|
716
1464
|
project = created.project;
|
|
717
1465
|
const next = { ...settings, projectID: project.id, key: created.projectKey };
|
|
718
1466
|
await writeProjectEnv(root, runtimeURL, project.id, created.projectKey);
|
|
@@ -724,38 +1472,48 @@ async function ensureProjectSettings(root, settings, options) {
|
|
|
724
1472
|
rl.close();
|
|
725
1473
|
}
|
|
726
1474
|
}
|
|
727
|
-
async function findRuntimeProject(runtimeURL, projectID) {
|
|
1475
|
+
async function findRuntimeProject(runtimeURL, projectID, accountToken) {
|
|
728
1476
|
const wanted = projectID.trim();
|
|
729
1477
|
if (!wanted)
|
|
730
1478
|
return null;
|
|
731
|
-
const projects = await fetchRuntimeProjects(runtimeURL);
|
|
1479
|
+
const projects = await fetchRuntimeProjects(runtimeURL, accountToken);
|
|
732
1480
|
return projects.find((project) => project.id === wanted) ?? null;
|
|
733
1481
|
}
|
|
734
|
-
async function fetchRuntimeProjects(runtimeURL) {
|
|
735
|
-
const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const payload = await response
|
|
1482
|
+
async function fetchRuntimeProjects(runtimeURL, accountToken) {
|
|
1483
|
+
const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects`, {
|
|
1484
|
+
headers: accountToken ? accountHeaders(accountToken) : undefined,
|
|
1485
|
+
});
|
|
1486
|
+
const payload = await runtimeJSON(response);
|
|
739
1487
|
return payload.projects ?? [];
|
|
740
1488
|
}
|
|
741
|
-
async function createRuntimeProject(runtimeURL, name) {
|
|
1489
|
+
async function createRuntimeProject(runtimeURL, name, accountToken, databaseMode = "single") {
|
|
742
1490
|
const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects`, {
|
|
743
1491
|
method: "POST",
|
|
744
|
-
headers: { "content-type": "application/json" },
|
|
745
|
-
body: JSON.stringify({ name }),
|
|
1492
|
+
headers: { ...(accountToken ? accountHeaders(accountToken) : {}), "content-type": "application/json" },
|
|
1493
|
+
body: JSON.stringify({ name, databaseMode }),
|
|
746
1494
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1495
|
+
const payload = await runtimeJSON(response);
|
|
1496
|
+
if (!payload.project?.id)
|
|
1497
|
+
throw new Error("runtime did not return project details");
|
|
750
1498
|
if (!payload.projectKey)
|
|
751
1499
|
throw new Error("runtime did not return a project key");
|
|
752
1500
|
return { project: payload.project, projectKey: payload.projectKey };
|
|
753
1501
|
}
|
|
1502
|
+
async function fetchRuntimeProjectKey(runtimeURL, projectID, accountToken) {
|
|
1503
|
+
const response = await fetch(`${runtimeURL.replace(/\/$/, "")}/dev/projects/${encodeURIComponent(projectID)}/key`, {
|
|
1504
|
+
method: "POST",
|
|
1505
|
+
headers: accountToken ? accountHeaders(accountToken) : undefined,
|
|
1506
|
+
});
|
|
1507
|
+
const payload = await runtimeJSON(response);
|
|
1508
|
+
if (!payload.projectKey)
|
|
1509
|
+
throw new Error("runtime did not return a project key");
|
|
1510
|
+
return payload.projectKey;
|
|
1511
|
+
}
|
|
754
1512
|
async function promptDefault(rl, label, fallback) {
|
|
755
1513
|
const answer = (await rl.question(`${label} (${fallback}): `)).trim();
|
|
756
1514
|
return answer || fallback;
|
|
757
1515
|
}
|
|
758
|
-
async function writeProjectEnv(root, runtimeURL, projectID, projectKey) {
|
|
1516
|
+
async function writeProjectEnv(root, runtimeURL, projectID, projectKey, overwrite = false) {
|
|
759
1517
|
await upsertEnvLocal(root, {
|
|
760
1518
|
GONVEX_PROJECT_ID: projectID,
|
|
761
1519
|
GONVEX_RUNTIME_URL: runtimeURL,
|
|
@@ -763,9 +1521,9 @@ async function writeProjectEnv(root, runtimeURL, projectID, projectKey) {
|
|
|
763
1521
|
VITE_GONVEX_PROJECT_ID: projectID,
|
|
764
1522
|
VITE_GONVEX_URL: runtimeURL,
|
|
765
1523
|
VITE_GONVEX_WS_URL: webSocketURL(runtimeURL),
|
|
766
|
-
});
|
|
1524
|
+
}, overwrite);
|
|
767
1525
|
}
|
|
768
|
-
async function upsertEnvLocal(root, values) {
|
|
1526
|
+
async function upsertEnvLocal(root, values, overwrite = false) {
|
|
769
1527
|
const envPath = join(root, ".env.local");
|
|
770
1528
|
const existing = existsSync(envPath) ? await readFile(envPath, "utf8") : "";
|
|
771
1529
|
const seen = new Set();
|
|
@@ -780,7 +1538,7 @@ async function upsertEnvLocal(root, values) {
|
|
|
780
1538
|
if (seen.has(key))
|
|
781
1539
|
return [];
|
|
782
1540
|
seen.add(key);
|
|
783
|
-
if (envLineValue(line) !== "")
|
|
1541
|
+
if (!overwrite && envLineValue(line) !== "")
|
|
784
1542
|
return [line];
|
|
785
1543
|
return [`${key}=${values[key]}`];
|
|
786
1544
|
});
|
|
@@ -788,7 +1546,8 @@ async function upsertEnvLocal(root, values) {
|
|
|
788
1546
|
if (!seen.has(key))
|
|
789
1547
|
next.push(`${key}=${value}`);
|
|
790
1548
|
}
|
|
791
|
-
await writeFile(envPath, `${next.join("\n")}\n
|
|
1549
|
+
await writeFile(envPath, `${next.join("\n")}\n`, { mode: 0o600 });
|
|
1550
|
+
await chmod(envPath, 0o600).catch(() => { });
|
|
792
1551
|
}
|
|
793
1552
|
function envLineValue(line) {
|
|
794
1553
|
const index = line.indexOf("=");
|
|
@@ -886,21 +1645,172 @@ function shouldPrintRuntimeLogEntry(entry, options) {
|
|
|
886
1645
|
const outcome = String(entry.outcome ?? "").toLowerCase();
|
|
887
1646
|
return Boolean(entry.error) || outcome === "error" || outcome === "warn" || outcome === "warning";
|
|
888
1647
|
}
|
|
1648
|
+
function normalizeRuntimeURL(value) {
|
|
1649
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
1650
|
+
if (!trimmed)
|
|
1651
|
+
return defaultRuntimeURL;
|
|
1652
|
+
let parsed;
|
|
1653
|
+
try {
|
|
1654
|
+
parsed = new URL(trimmed);
|
|
1655
|
+
}
|
|
1656
|
+
catch {
|
|
1657
|
+
throw new Error(`invalid runtime URL ${value}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1660
|
+
throw new Error("runtime URL must use http or https");
|
|
1661
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
1662
|
+
}
|
|
1663
|
+
function cliConfigPath() {
|
|
1664
|
+
if (process.env.GONVEX_CONFIG_PATH)
|
|
1665
|
+
return resolve(process.env.GONVEX_CONFIG_PATH);
|
|
1666
|
+
const configRoot = process.env.XDG_CONFIG_HOME ? resolve(process.env.XDG_CONFIG_HOME) : join(homedir(), ".config");
|
|
1667
|
+
return join(configRoot, "gonvex", "config.json");
|
|
1668
|
+
}
|
|
1669
|
+
function emptyCLIConfig() {
|
|
1670
|
+
return { version: 1, runtimes: {} };
|
|
1671
|
+
}
|
|
1672
|
+
async function readCLIConfig() {
|
|
1673
|
+
const path = cliConfigPath();
|
|
1674
|
+
if (!existsSync(path))
|
|
1675
|
+
return emptyCLIConfig();
|
|
1676
|
+
let parsed;
|
|
1677
|
+
try {
|
|
1678
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1679
|
+
}
|
|
1680
|
+
catch (error) {
|
|
1681
|
+
throw new Error(`could not read Gonvex CLI config ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
version: 1,
|
|
1685
|
+
currentRuntime: parsed.currentRuntime,
|
|
1686
|
+
runtimes: parsed.runtimes && typeof parsed.runtimes === "object" ? parsed.runtimes : {},
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
async function writeCLIConfig(config) {
|
|
1690
|
+
const path = cliConfigPath();
|
|
1691
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
1692
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
1693
|
+
await chmod(path, 0o600).catch(() => { });
|
|
1694
|
+
}
|
|
1695
|
+
async function saveAccountProfile(runtimeURL, accessToken, identity, expiresAt) {
|
|
1696
|
+
runtimeURL = normalizeRuntimeURL(runtimeURL);
|
|
1697
|
+
const config = await readCLIConfig();
|
|
1698
|
+
config.currentRuntime = runtimeURL;
|
|
1699
|
+
config.runtimes[runtimeURL] = {
|
|
1700
|
+
runtimeURL,
|
|
1701
|
+
accessToken,
|
|
1702
|
+
email: identity.account.email,
|
|
1703
|
+
name: identity.account.name,
|
|
1704
|
+
authentication: identity.authentication,
|
|
1705
|
+
permissions: identity.permissions,
|
|
1706
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
1707
|
+
};
|
|
1708
|
+
await writeCLIConfig(config);
|
|
1709
|
+
}
|
|
1710
|
+
async function removeAccountProfile(runtimeURL) {
|
|
1711
|
+
runtimeURL = normalizeRuntimeURL(runtimeURL);
|
|
1712
|
+
const config = await readCLIConfig();
|
|
1713
|
+
if (!config.runtimes[runtimeURL])
|
|
1714
|
+
return false;
|
|
1715
|
+
delete config.runtimes[runtimeURL];
|
|
1716
|
+
if (config.currentRuntime === runtimeURL)
|
|
1717
|
+
config.currentRuntime = Object.keys(config.runtimes)[0];
|
|
1718
|
+
await writeCLIConfig(config);
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
async function accountRuntimeForArgs(argv) {
|
|
1722
|
+
const explicit = valueFor(argv, "--runtime-url") ?? valueFor(argv, "--runtime") ?? process.env.GONVEX_RUNTIME_URL;
|
|
1723
|
+
if (explicit)
|
|
1724
|
+
return normalizeRuntimeURL(explicit);
|
|
1725
|
+
const config = await readCLIConfig();
|
|
1726
|
+
return normalizeRuntimeURL(config.currentRuntime ?? defaultRuntimeURL);
|
|
1727
|
+
}
|
|
1728
|
+
async function accountAccessTokenForRuntime(runtimeURL) {
|
|
1729
|
+
if (process.env.GONVEX_ACCOUNT_TOKEN?.trim())
|
|
1730
|
+
return process.env.GONVEX_ACCOUNT_TOKEN.trim();
|
|
1731
|
+
const config = await readCLIConfig();
|
|
1732
|
+
const profile = config.runtimes[normalizeRuntimeURL(runtimeURL)];
|
|
1733
|
+
if (profile?.expiresAt && profile.expiresAt <= Date.now()) {
|
|
1734
|
+
throw new Error(`saved login for ${normalizeRuntimeURL(runtimeURL)} has expired; run 'gonvex login' again`);
|
|
1735
|
+
}
|
|
1736
|
+
return profile?.accessToken;
|
|
1737
|
+
}
|
|
1738
|
+
async function requireAccountAccessToken(runtimeURL) {
|
|
1739
|
+
const token = await accountAccessTokenForRuntime(runtimeURL);
|
|
1740
|
+
if (!token)
|
|
1741
|
+
throw new Error(`not logged in to ${normalizeRuntimeURL(runtimeURL)}; run 'gonvex login --runtime-url ${normalizeRuntimeURL(runtimeURL)}'`);
|
|
1742
|
+
return token;
|
|
1743
|
+
}
|
|
1744
|
+
function accountHeaders(accessToken) {
|
|
1745
|
+
return { authorization: `Bearer ${accessToken}` };
|
|
1746
|
+
}
|
|
1747
|
+
async function runtimeJSON(response) {
|
|
1748
|
+
const text = await response.text();
|
|
1749
|
+
let payload = {};
|
|
1750
|
+
if (text) {
|
|
1751
|
+
try {
|
|
1752
|
+
payload = JSON.parse(text);
|
|
1753
|
+
}
|
|
1754
|
+
catch {
|
|
1755
|
+
payload = { error: text };
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (!response.ok) {
|
|
1759
|
+
const error = payload && typeof payload === "object" && "error" in payload ? String(payload.error) : response.statusText;
|
|
1760
|
+
const permission = payload && typeof payload === "object" && "permission" in payload ? ` (${String(payload.permission)})` : "";
|
|
1761
|
+
throw new Error(`runtime returned ${response.status}: ${error}${permission}`);
|
|
1762
|
+
}
|
|
1763
|
+
return payload;
|
|
1764
|
+
}
|
|
1765
|
+
async function fetchAccountIdentity(runtimeURL, accessToken) {
|
|
1766
|
+
const response = await fetch(`${normalizeRuntimeURL(runtimeURL)}/dev/auth/me`, { headers: accountHeaders(accessToken) });
|
|
1767
|
+
return runtimeJSON(response);
|
|
1768
|
+
}
|
|
1769
|
+
async function promptHidden(label) {
|
|
1770
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
1771
|
+
throw new Error("password must be provided non-interactively with --password or GONVEX_PASSWORD");
|
|
1772
|
+
let hidden = false;
|
|
1773
|
+
const output = new Writable({
|
|
1774
|
+
write(chunk, encoding, callback) {
|
|
1775
|
+
if (!hidden)
|
|
1776
|
+
process.stdout.write(chunk, encoding);
|
|
1777
|
+
callback();
|
|
1778
|
+
},
|
|
1779
|
+
});
|
|
1780
|
+
const rl = createInterface({ input: process.stdin, output, terminal: true });
|
|
1781
|
+
try {
|
|
1782
|
+
const pending = rl.question(label);
|
|
1783
|
+
hidden = true;
|
|
1784
|
+
const answer = await pending;
|
|
1785
|
+
hidden = false;
|
|
1786
|
+
process.stdout.write("\n");
|
|
1787
|
+
return answer;
|
|
1788
|
+
}
|
|
1789
|
+
finally {
|
|
1790
|
+
rl.close();
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
889
1793
|
async function loadSettings(root, overrides) {
|
|
890
1794
|
loadDotEnv(join(root, ".env.local"));
|
|
891
1795
|
loadDotEnv(join(root, ".env"));
|
|
892
1796
|
const config = await loadConfig(root);
|
|
1797
|
+
const cliConfig = await readCLIConfig();
|
|
893
1798
|
const key = overrides.key ?? process.env.GONVEX_PROJECT_KEY ?? process.env.GONVEX_DEPLOY_KEY ?? process.env.GONVEX_KEY ?? "";
|
|
894
1799
|
const explicitProjectID = overrides.projectID ?? process.env.GONVEX_PROJECT_ID ?? process.env.GONVEX_PROJECT ?? config.project;
|
|
1800
|
+
const overriddenProjectID = overrides.projectID?.trim() || undefined;
|
|
1801
|
+
const keyedProjectID = projectIDFromKey(key);
|
|
1802
|
+
if (overriddenProjectID && keyedProjectID && overriddenProjectID !== keyedProjectID) {
|
|
1803
|
+
throw new Error(`project id ${JSON.stringify(overriddenProjectID)} does not match the project key for ${JSON.stringify(keyedProjectID)}`);
|
|
1804
|
+
}
|
|
895
1805
|
return {
|
|
896
|
-
projectID:
|
|
897
|
-
runtimeURL: overrides.runtimeURL ?? process.env.GONVEX_RUNTIME_URL ?? config.runtime ?? defaultRuntimeURL,
|
|
1806
|
+
projectID: overriddenProjectID ?? keyedProjectID ?? (explicitProjectID?.trim() || basename(root)),
|
|
1807
|
+
runtimeURL: normalizeRuntimeURL(overrides.runtimeURL ?? process.env.GONVEX_RUNTIME_URL ?? config.runtime ?? cliConfig.currentRuntime ?? defaultRuntimeURL),
|
|
898
1808
|
key,
|
|
899
1809
|
};
|
|
900
1810
|
}
|
|
901
1811
|
function projectIDFromKey(key) {
|
|
902
1812
|
const trimmed = key.trim();
|
|
903
|
-
if (!trimmed.startsWith("gvx_"))
|
|
1813
|
+
if (!trimmed.startsWith("gvx_") || trimmed.startsWith("gvx_pat_"))
|
|
904
1814
|
return undefined;
|
|
905
1815
|
const payload = trimmed.slice("gvx_".length);
|
|
906
1816
|
let encodedProject = payload.split(".", 1)[0];
|
|
@@ -981,7 +1891,7 @@ async function copyDir(source, target, overwrite) {
|
|
|
981
1891
|
await mkdir(target, { recursive: true });
|
|
982
1892
|
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
983
1893
|
const sourcePath = join(source, entry.name);
|
|
984
|
-
const targetPath = join(target, entry.name);
|
|
1894
|
+
const targetPath = join(target, entry.name === "_gitignore" ? ".gitignore" : entry.name);
|
|
985
1895
|
if (entry.isDirectory()) {
|
|
986
1896
|
await copyDir(sourcePath, targetPath, overwrite);
|
|
987
1897
|
}
|
|
@@ -1011,7 +1921,7 @@ async function writeEnvLocal(root, project, runtime) {
|
|
|
1011
1921
|
if (existsSync(envPath))
|
|
1012
1922
|
return;
|
|
1013
1923
|
const wsURL = runtime.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/$/, "") + "/ws";
|
|
1014
|
-
await writeFile(envPath, `GONVEX_PROJECT_ID=${project}\nGONVEX_RUNTIME_URL=${runtime}\nGONVEX_PROJECT_KEY=\nVITE_GONVEX_WS_URL=${wsURL}\n`);
|
|
1924
|
+
await writeFile(envPath, `GONVEX_PROJECT_ID=${project}\nGONVEX_RUNTIME_URL=${runtime}\nGONVEX_PROJECT_KEY=\nVITE_GONVEX_PROJECT_ID=${project}\nVITE_GONVEX_URL=${runtime}\nVITE_GONVEX_WS_URL=${wsURL}\n`);
|
|
1015
1925
|
}
|
|
1016
1926
|
function templateDir(template) {
|
|
1017
1927
|
const packageTemplate = resolve(dirname(fileURLToPath(import.meta.url)), "templates", template);
|
|
@@ -1024,6 +1934,8 @@ function functionKind(raw) {
|
|
|
1024
1934
|
return "internalMutation";
|
|
1025
1935
|
if (raw === "LiveGrid")
|
|
1026
1936
|
return "liveGrid";
|
|
1937
|
+
if (raw === "PublicHTTP")
|
|
1938
|
+
return "http";
|
|
1027
1939
|
return raw.toLowerCase();
|
|
1028
1940
|
}
|
|
1029
1941
|
function columnType(kind) {
|
|
@@ -1044,6 +1956,29 @@ function valueFor(args, key) {
|
|
|
1044
1956
|
return undefined;
|
|
1045
1957
|
return args[index + 1];
|
|
1046
1958
|
}
|
|
1959
|
+
function valuesFor(args, key) {
|
|
1960
|
+
const values = [];
|
|
1961
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1962
|
+
if (args[index] === key && args[index + 1] !== undefined) {
|
|
1963
|
+
values.push(args[index + 1]);
|
|
1964
|
+
index += 1;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return values;
|
|
1968
|
+
}
|
|
1969
|
+
function positionalArgs(args, optionsWithValues) {
|
|
1970
|
+
const positional = [];
|
|
1971
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1972
|
+
const arg = args[index];
|
|
1973
|
+
if (optionsWithValues.includes(arg)) {
|
|
1974
|
+
index += 1;
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
if (!arg.startsWith("-"))
|
|
1978
|
+
positional.push(arg);
|
|
1979
|
+
}
|
|
1980
|
+
return positional;
|
|
1981
|
+
}
|
|
1047
1982
|
function basename(path) {
|
|
1048
1983
|
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
1049
1984
|
return parts.at(-1) ?? "app";
|
|
@@ -1058,11 +1993,31 @@ function sleep(ms, signal) {
|
|
|
1058
1993
|
});
|
|
1059
1994
|
}
|
|
1060
1995
|
function printHelp() {
|
|
1061
|
-
console.log("Usage: gonvex <dev|init|create|env> [options]");
|
|
1996
|
+
console.log("Usage: gonvex <dev|init|create|env|auth|login|logout|whoami|project|token> [options]");
|
|
1062
1997
|
console.log(" gonvex dev [--project <path>] [--runtime-url <url>] [--project-id <id>] [--key <key>] [--once] [--verbose-logs] [-- <command>]");
|
|
1063
1998
|
console.log(" gonvex init [--template vite-react] [--project <id>] [--runtime <url>]");
|
|
1064
|
-
console.log(" gonvex create <app-name> [--
|
|
1065
|
-
console.log(" gonvex env <list|get|set|remove> [--project <path>] [--runtime-url <url>] [--project-id <id>] [--key <key>]");
|
|
1999
|
+
console.log(" gonvex create <app-name> [--runtime-url <url>] [--provision] [--database-mode single|multiTenant] [--google-auth] [--origin <url>]... [--signup-mode personal|inviteOnly] [--owner <email>]");
|
|
2000
|
+
console.log(" gonvex env <list|get|set|push|remove> [--project <path>] [--runtime-url <url>] [--project-id <id>] [--key <key>]");
|
|
2001
|
+
console.log(" gonvex auth <add|remove|status|users> [google] [options]");
|
|
2002
|
+
console.log(" gonvex login [--runtime-url <url>] [--email <email> | --token <personal-access-token>]");
|
|
2003
|
+
console.log(" gonvex logout [--runtime-url <url>]");
|
|
2004
|
+
console.log(" gonvex whoami [--runtime-url <url>] [--json]");
|
|
2005
|
+
console.log(" gonvex project <list|create|select> [options]");
|
|
2006
|
+
console.log(" gonvex token <list|create|revoke|permissions> [options]");
|
|
2007
|
+
}
|
|
2008
|
+
function printAuthHelp() {
|
|
2009
|
+
console.log("Usage: gonvex auth <command> [options]");
|
|
2010
|
+
console.log(" gonvex auth add google [--origin URL]... [--callback-path /] [--signup-mode personal|inviteOnly]");
|
|
2011
|
+
console.log(" gonvex auth remove google [--origin URL]... [--callback-path /]");
|
|
2012
|
+
console.log(" gonvex auth status [--json]");
|
|
2013
|
+
console.log(" gonvex auth doctor [--json]");
|
|
2014
|
+
console.log(" gonvex auth users [--json]");
|
|
2015
|
+
console.log(" gonvex auth tenants list [--json]");
|
|
2016
|
+
console.log(" gonvex auth tenants create <name> [--owner <email>]");
|
|
2017
|
+
console.log(" gonvex auth memberships list --tenant <tenant-id> [--json]");
|
|
2018
|
+
console.log(" gonvex auth memberships add --tenant <tenant-id> --email <email> [--role member]");
|
|
2019
|
+
console.log(" gonvex auth memberships remove --tenant <tenant-id> (--user <user-id> | --email <invited-email>)");
|
|
2020
|
+
console.log(" gonvex auth user <disable|enable|delete> <user-id>");
|
|
1066
2021
|
}
|
|
1067
2022
|
function printEnvHelp() {
|
|
1068
2023
|
console.log("Usage: gonvex env <command> [options]");
|
|
@@ -1070,8 +2025,23 @@ function printEnvHelp() {
|
|
|
1070
2025
|
console.log(" gonvex env get NAME");
|
|
1071
2026
|
console.log(" gonvex env set NAME VALUE");
|
|
1072
2027
|
console.log(" gonvex env set NAME=VALUE");
|
|
2028
|
+
console.log(" gonvex env push FILE");
|
|
2029
|
+
console.log(" gonvex env push --file FILE");
|
|
1073
2030
|
console.log(" gonvex env remove NAME");
|
|
1074
2031
|
}
|
|
2032
|
+
function printProjectHelp() {
|
|
2033
|
+
console.log("Usage: gonvex project <command> [options]");
|
|
2034
|
+
console.log(" gonvex project list [--runtime-url <url>] [--json]");
|
|
2035
|
+
console.log(" gonvex project create [name] [--runtime-url <url>] [--database-mode single|multiTenant] [--project-root <path>] [--json]");
|
|
2036
|
+
console.log(" gonvex project select <project-id> [--runtime-url <url>] [--project-root <path>]");
|
|
2037
|
+
}
|
|
2038
|
+
function printTokenHelp() {
|
|
2039
|
+
console.log("Usage: gonvex token <command> [options]");
|
|
2040
|
+
console.log(" gonvex token list [--runtime-url <url>] [--json]");
|
|
2041
|
+
console.log(" gonvex token create [name] [--permission <permission>]... [--full] [--expires-at <RFC3339>] [--json]");
|
|
2042
|
+
console.log(" gonvex token revoke <token-id> [--runtime-url <url>]");
|
|
2043
|
+
console.log(" gonvex token permissions");
|
|
2044
|
+
}
|
|
1075
2045
|
function isCliEntrypoint() {
|
|
1076
2046
|
const invokedPath = process.argv[1];
|
|
1077
2047
|
if (!invokedPath)
|