@robelest/convex-auth 0.0.2-preview.2 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/bin.cjs +466 -63
  2. package/dist/client/index.d.ts +127 -0
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +414 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +56 -1
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +93 -3
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/convex.config.d.ts.map +1 -1
  12. package/dist/component/convex.config.js +2 -0
  13. package/dist/component/convex.config.js.map +1 -1
  14. package/dist/component/index.d.ts +5 -3
  15. package/dist/component/index.d.ts.map +1 -1
  16. package/dist/component/index.js +5 -3
  17. package/dist/component/index.js.map +1 -1
  18. package/dist/component/portalBridge.d.ts +80 -0
  19. package/dist/component/portalBridge.d.ts.map +1 -0
  20. package/dist/component/portalBridge.js +102 -0
  21. package/dist/component/portalBridge.js.map +1 -0
  22. package/dist/component/public.d.ts +193 -9
  23. package/dist/component/public.d.ts.map +1 -1
  24. package/dist/component/public.js +204 -33
  25. package/dist/component/public.js.map +1 -1
  26. package/dist/component/schema.d.ts +89 -9
  27. package/dist/component/schema.d.ts.map +1 -1
  28. package/dist/component/schema.js +68 -7
  29. package/dist/component/schema.js.map +1 -1
  30. package/dist/providers/passkey.d.ts +20 -0
  31. package/dist/providers/passkey.d.ts.map +1 -0
  32. package/dist/providers/passkey.js +32 -0
  33. package/dist/providers/passkey.js.map +1 -0
  34. package/dist/providers/totp.d.ts +14 -0
  35. package/dist/providers/totp.d.ts.map +1 -0
  36. package/dist/providers/totp.js +23 -0
  37. package/dist/providers/totp.js.map +1 -0
  38. package/dist/server/convex-auth.d.ts +243 -0
  39. package/dist/server/convex-auth.d.ts.map +1 -0
  40. package/dist/server/convex-auth.js +365 -0
  41. package/dist/server/convex-auth.js.map +1 -0
  42. package/dist/server/implementation/index.d.ts +80 -7
  43. package/dist/server/implementation/index.d.ts.map +1 -1
  44. package/dist/server/implementation/index.js +88 -5
  45. package/dist/server/implementation/index.js.map +1 -1
  46. package/dist/server/implementation/passkey.d.ts +33 -0
  47. package/dist/server/implementation/passkey.d.ts.map +1 -0
  48. package/dist/server/implementation/passkey.js +450 -0
  49. package/dist/server/implementation/passkey.js.map +1 -0
  50. package/dist/server/implementation/redirects.d.ts.map +1 -1
  51. package/dist/server/implementation/redirects.js +4 -9
  52. package/dist/server/implementation/redirects.js.map +1 -1
  53. package/dist/server/implementation/signIn.d.ts +13 -0
  54. package/dist/server/implementation/signIn.d.ts.map +1 -1
  55. package/dist/server/implementation/signIn.js +26 -1
  56. package/dist/server/implementation/signIn.js.map +1 -1
  57. package/dist/server/implementation/totp.d.ts +40 -0
  58. package/dist/server/implementation/totp.d.ts.map +1 -0
  59. package/dist/server/implementation/totp.js +211 -0
  60. package/dist/server/implementation/totp.js.map +1 -0
  61. package/dist/server/portal-email.d.ts +19 -0
  62. package/dist/server/portal-email.d.ts.map +1 -0
  63. package/dist/server/portal-email.js +89 -0
  64. package/dist/server/portal-email.js.map +1 -0
  65. package/dist/server/portal.d.ts +116 -0
  66. package/dist/server/portal.d.ts.map +1 -0
  67. package/dist/server/portal.js +294 -0
  68. package/dist/server/portal.js.map +1 -0
  69. package/dist/server/provider_utils.d.ts +1 -1
  70. package/dist/server/provider_utils.d.ts.map +1 -1
  71. package/dist/server/provider_utils.js +39 -1
  72. package/dist/server/provider_utils.js.map +1 -1
  73. package/dist/server/types.d.ts +58 -2
  74. package/dist/server/types.d.ts.map +1 -1
  75. package/package.json +5 -2
  76. package/src/cli/index.ts +48 -6
  77. package/src/cli/portal-link.ts +112 -0
  78. package/src/cli/portal-upload.ts +411 -0
  79. package/src/client/index.ts +477 -0
  80. package/src/component/_generated/api.ts +72 -1
  81. package/src/component/_generated/component.ts +180 -4
  82. package/src/component/convex.config.ts +3 -0
  83. package/src/component/index.ts +5 -3
  84. package/src/component/portalBridge.ts +116 -0
  85. package/src/component/public.ts +231 -37
  86. package/src/component/schema.ts +70 -7
  87. package/src/providers/passkey.ts +35 -0
  88. package/src/providers/totp.ts +26 -0
  89. package/src/server/convex-auth.ts +470 -0
  90. package/src/server/implementation/index.ts +109 -8
  91. package/src/server/implementation/passkey.ts +650 -0
  92. package/src/server/implementation/redirects.ts +4 -11
  93. package/src/server/implementation/signIn.ts +39 -1
  94. package/src/server/implementation/totp.ts +366 -0
  95. package/src/server/portal-email.ts +95 -0
  96. package/src/server/portal.ts +375 -0
  97. package/src/server/provider_utils.ts +42 -1
  98. package/src/server/types.ts +66 -2
package/src/cli/index.ts CHANGED
@@ -11,12 +11,50 @@ import * as v from "valibot";
11
11
  import { actionDescription } from "./command.js";
12
12
  import { generateKeys } from "./generateKeys.js";
13
13
 
14
- new Command()
14
+ const program = new Command()
15
15
  .name("@robelest/convex-auth")
16
16
  .description(
17
17
  "Add code and set environment variables for @robelest/convex-auth.\n\n" +
18
18
  "Full docs: https://deepwiki.com/robelest/convex-auth",
19
+ );
20
+
21
+ // ---- Portal subcommand ----
22
+ const portalCmd = program
23
+ .command("portal")
24
+ .description("Manage the auth admin portal");
25
+
26
+ portalCmd
27
+ .command("upload")
28
+ .description("Upload portal static files to Convex storage")
29
+ .allowUnknownOption(true)
30
+ .allowExcessArguments(true)
31
+ .action(async () => {
32
+ // Pass remaining args after "portal upload" to the upload handler
33
+ const idx = process.argv.indexOf("upload");
34
+ const uploadArgs = idx >= 0 ? process.argv.slice(idx + 1) : [];
35
+ const { portalUploadMain } = await import("./portal-upload.js");
36
+ await portalUploadMain(uploadArgs);
37
+ });
38
+
39
+ portalCmd
40
+ .command("link")
41
+ .description("Generate an admin invite link for the portal")
42
+ .option("--prod", "Use production deployment")
43
+ .option(
44
+ "--component <name>",
45
+ "Convex module with portal functions",
46
+ "auth",
19
47
  )
48
+ .action(async (opts) => {
49
+ const { portalLinkMain } = await import("./portal-link.js");
50
+ await portalLinkMain({
51
+ prod: opts.prod ?? false,
52
+ component: opts.component,
53
+ });
54
+ });
55
+
56
+ // ---- Default setup command ----
57
+ program
20
58
  .option(
21
59
  "--site-url <url>",
22
60
  "Your frontend app URL (e.g. 'http://localhost:5173' for dev, 'https://myapp.com' for prod)",
@@ -93,8 +131,9 @@ new Command()
93
131
  } else {
94
132
  printFinalSuccessMessage(config);
95
133
  }
96
- })
97
- .parse(process.argv);
134
+ });
135
+
136
+ program.parse(process.argv);
98
137
 
99
138
  type ProjectConfig = {
100
139
  isExpo: boolean;
@@ -432,13 +471,16 @@ export default app;
432
471
  async function initializeAuth(config: ProjectConfig) {
433
472
  logStep(config, "Initialize auth file");
434
473
  const sourceTemplate = `\
435
- import { Auth } from "@robelest/convex-auth/component";
474
+ import { Auth, Portal } from "@robelest/convex-auth/component";
436
475
  import { components } from "./_generated/api";
437
476
 
438
- export const { auth, signIn, signOut, store } = Auth({$$
439
- component: components.auth,$$
477
+ const auth = new Auth(components.auth, {$$
440
478
  providers: [$$],$$
441
479
  });
480
+
481
+ export { auth };
482
+ export const { signIn, signOut, store } = auth;
483
+ export const { portalQuery, portalMutation, portalInternal } = Portal(auth);
442
484
  `;
443
485
  const source = templateToSource(sourceTemplate);
444
486
  const authPath = path.join(config.convexFolderPath, "auth");
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool to generate a portal admin invite link.
4
+ *
5
+ * Generates a random invite token, hashes it (SHA-256), stores the hash
6
+ * in the database via `createPortalInvite`, and prints a URL the admin
7
+ * can visit to accept the invite.
8
+ *
9
+ * Usage:
10
+ * npx @robelest/convex-auth portal link [options]
11
+ *
12
+ * Options:
13
+ * --prod Use production deployment
14
+ * --component <name> Convex component with portal functions (default: portal)
15
+ */
16
+
17
+ import { randomBytes, createHash } from "crypto";
18
+ import { execFile } from "child_process";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ let useProd = false;
25
+
26
+ /**
27
+ * Run a Convex internal function via the CLI.
28
+ * Follows the same pattern as portal-upload.ts.
29
+ */
30
+ function convexRunAsync(
31
+ functionPath: string,
32
+ args: Record<string, unknown> = {},
33
+ ): Promise<string> {
34
+ return new Promise((resolve, reject) => {
35
+ const cmdArgs = [
36
+ "convex",
37
+ "run",
38
+ functionPath,
39
+ JSON.stringify(args),
40
+ "--typecheck=disable",
41
+ "--codegen=disable",
42
+ ];
43
+ if (useProd) cmdArgs.push("--prod");
44
+ execFile("npx", cmdArgs, { encoding: "utf-8" }, (error, stdout, stderr) => {
45
+ if (error) {
46
+ console.error("Convex run failed:", stderr || stdout);
47
+ reject(error);
48
+ return;
49
+ }
50
+ resolve(stdout.trim());
51
+ });
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Generate a URL-safe random token (32 bytes → 43 chars base64url).
57
+ */
58
+ function generateToken(): string {
59
+ return randomBytes(32).toString("base64url");
60
+ }
61
+
62
+ /**
63
+ * SHA-256 hash a token and return the hex digest.
64
+ */
65
+ function hashToken(token: string): string {
66
+ return createHash("sha256").update(token).digest("hex");
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Main
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export async function portalLinkMain(opts: {
74
+ prod: boolean;
75
+ component: string;
76
+ }): Promise<void> {
77
+ useProd = opts.prod;
78
+ const component = opts.component;
79
+
80
+ // 1. Generate a random token and its hash
81
+ const token = generateToken();
82
+ const tokenHash = hashToken(token);
83
+
84
+ console.log("Creating portal admin invite...");
85
+
86
+ // 2. Store the invite and get the portal URL back
87
+ let portalUrl: string;
88
+ try {
89
+ const raw = await convexRunAsync(`${component}:portalInternal`, {
90
+ action: "createPortalInvite",
91
+ tokenHash,
92
+ });
93
+ const result = JSON.parse(raw);
94
+ portalUrl = result.portalUrl;
95
+ } catch {
96
+ console.error(
97
+ "\nFailed to create invite. Make sure your Convex deployment is running",
98
+ "and the portal module is configured in your convex/ directory.",
99
+ );
100
+ process.exit(1);
101
+ }
102
+
103
+ // 3. Print the invite link
104
+ const inviteUrl = `${portalUrl}?invite=${token}`;
105
+
106
+ console.log("\nPortal admin invite created!\n");
107
+ console.log(` ${inviteUrl}\n`);
108
+ console.log("This invite is single-use. Share it securely.");
109
+ if (useProd) {
110
+ console.log("(Using production deployment)");
111
+ }
112
+ }
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool to upload portal static files to Convex storage.
4
+ *
5
+ * Forked from @convex-dev/self-hosting upload, adapted to use the
6
+ * consolidated `portalInternal` internal mutation with action discriminator.
7
+ *
8
+ * Usage:
9
+ * npx @robelest/convex-auth portal upload [options]
10
+ *
11
+ * Options:
12
+ * --dist <path> Path to dist directory (default: ./dist)
13
+ * --component <name> Convex module with portal functions (default: auth)
14
+ * --prod Deploy to production deployment
15
+ * --help Show help
16
+ */
17
+
18
+ import { readFileSync, readdirSync, existsSync } from "fs";
19
+ import { join, relative, extname, resolve } from "path";
20
+ import { randomUUID } from "crypto";
21
+ import { execSync, execFile, spawnSync } from "child_process";
22
+
23
+ // MIME type mapping
24
+ const MIME_TYPES: Record<string, string> = {
25
+ ".html": "text/html; charset=utf-8",
26
+ ".js": "application/javascript; charset=utf-8",
27
+ ".mjs": "application/javascript; charset=utf-8",
28
+ ".css": "text/css; charset=utf-8",
29
+ ".json": "application/json; charset=utf-8",
30
+ ".png": "image/png",
31
+ ".jpg": "image/jpeg",
32
+ ".jpeg": "image/jpeg",
33
+ ".gif": "image/gif",
34
+ ".svg": "image/svg+xml",
35
+ ".ico": "image/x-icon",
36
+ ".webp": "image/webp",
37
+ ".woff": "font/woff",
38
+ ".woff2": "font/woff2",
39
+ ".ttf": "font/ttf",
40
+ ".txt": "text/plain; charset=utf-8",
41
+ ".map": "application/json",
42
+ ".webmanifest": "application/manifest+json",
43
+ ".xml": "application/xml",
44
+ };
45
+
46
+ function getMimeType(path: string): string {
47
+ return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream";
48
+ }
49
+
50
+ interface ParsedArgs {
51
+ dist: string;
52
+ component: string;
53
+ prod: boolean;
54
+ build: boolean;
55
+ concurrency: number;
56
+ help: boolean;
57
+ }
58
+
59
+ export function parsePortalUploadArgs(args: string[]): ParsedArgs {
60
+ const result: ParsedArgs = {
61
+ dist: "./dist",
62
+ component: "auth",
63
+ prod: false,
64
+ build: false,
65
+ concurrency: 5,
66
+ help: false,
67
+ };
68
+
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (arg === "--help" || arg === "-h") {
72
+ result.help = true;
73
+ } else if (arg === "--dist" || arg === "-d") {
74
+ result.dist = args[++i] || result.dist;
75
+ } else if (arg === "--component" || arg === "-c") {
76
+ result.component = args[++i] || result.component;
77
+ } else if (arg === "--prod") {
78
+ result.prod = true;
79
+ } else if (arg === "--no-prod" || arg === "--dev") {
80
+ result.prod = false;
81
+ } else if (arg === "--build" || arg === "-b") {
82
+ result.build = true;
83
+ } else if (arg === "--concurrency" || arg === "-j") {
84
+ const val = parseInt(args[++i], 10);
85
+ if (val > 0) result.concurrency = val;
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function showHelp(): void {
93
+ console.log(`
94
+ Usage: npx @robelest/convex-auth portal upload [options]
95
+
96
+ Upload portal static files to Convex storage.
97
+
98
+ Options:
99
+ -d, --dist <path> Path to dist directory (default: ./dist)
100
+ -c, --component <name> Convex module with portal functions (default: auth)
101
+ --prod Deploy to production deployment
102
+ -b, --build Run 'npm run build' before uploading
103
+ -j, --concurrency <n> Number of parallel uploads (default: 5)
104
+ -h, --help Show this help message
105
+
106
+ Examples:
107
+ npx @robelest/convex-auth portal upload
108
+ npx @robelest/convex-auth portal upload --dist packages/portal/build --prod
109
+ npx @robelest/convex-auth portal upload --build --prod
110
+ `);
111
+ }
112
+
113
+ // Global flag for production mode
114
+ let useProd = true;
115
+
116
+ /**
117
+ * Run a Convex function via the CLI. Uses the consolidated `portalInternal`
118
+ * internal mutation with an `action` discriminator.
119
+ */
120
+ function convexRunAsync(
121
+ functionPath: string,
122
+ args: Record<string, unknown> = {},
123
+ ): Promise<string> {
124
+ return new Promise((resolve, reject) => {
125
+ const cmdArgs = [
126
+ "convex",
127
+ "run",
128
+ functionPath,
129
+ JSON.stringify(args),
130
+ "--typecheck=disable",
131
+ "--codegen=disable",
132
+ ];
133
+ if (useProd) cmdArgs.push("--prod");
134
+ execFile("npx", cmdArgs, { encoding: "utf-8" }, (error, stdout, stderr) => {
135
+ if (error) {
136
+ console.error("Convex run failed:", stderr || stdout);
137
+ reject(error);
138
+ return;
139
+ }
140
+ resolve(stdout.trim());
141
+ });
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Run the portalInternal function with a specific action.
147
+ */
148
+ function runHosting(
149
+ componentName: string,
150
+ action: string,
151
+ extraArgs: Record<string, unknown> = {},
152
+ ): Promise<string> {
153
+ return convexRunAsync(`${componentName}:portalInternal`, {
154
+ action,
155
+ ...extraArgs,
156
+ });
157
+ }
158
+
159
+ async function uploadSingleFile(
160
+ file: { path: string; localPath: string; contentType: string },
161
+ componentName: string,
162
+ deploymentId: string,
163
+ ): Promise<{ path: string }> {
164
+ const content = readFileSync(file.localPath);
165
+
166
+ // Generate upload URL
167
+ const uploadUrlOutput = await runHosting(componentName, "generateUploadUrl");
168
+ const uploadUrl = JSON.parse(uploadUrlOutput);
169
+
170
+ // Upload to Convex storage
171
+ const response = await fetch(uploadUrl, {
172
+ method: "POST",
173
+ headers: { "Content-Type": file.contentType },
174
+ body: content,
175
+ });
176
+
177
+ const { storageId } = (await response.json()) as { storageId: string };
178
+
179
+ // Record the asset
180
+ await runHosting(componentName, "recordAsset", {
181
+ path: file.path,
182
+ storageId,
183
+ contentType: file.contentType,
184
+ deploymentId,
185
+ });
186
+
187
+ return { path: file.path };
188
+ }
189
+
190
+ async function uploadWithConcurrency(
191
+ files: Array<{ path: string; localPath: string; contentType: string }>,
192
+ componentName: string,
193
+ deploymentId: string,
194
+ concurrency: number,
195
+ ): Promise<void> {
196
+ const total = files.length;
197
+ let completed = 0;
198
+ let failed = false;
199
+
200
+ const pending = new Set<Promise<void>>();
201
+ const iterator = files[Symbol.iterator]();
202
+
203
+ function enqueue(): Promise<void> | undefined {
204
+ if (failed) return;
205
+ const next = iterator.next();
206
+ if (next.done) return;
207
+ const file = next.value;
208
+
209
+ const task = uploadSingleFile(file, componentName, deploymentId).then(
210
+ ({ path }) => {
211
+ completed++;
212
+ console.log(` [${completed}/${total}] ${path}`);
213
+ pending.delete(task);
214
+ },
215
+ );
216
+
217
+ task.catch(() => {
218
+ failed = true;
219
+ });
220
+
221
+ pending.add(task);
222
+ return task;
223
+ }
224
+
225
+ // Fill initial pool
226
+ for (let i = 0; i < concurrency && i < total; i++) {
227
+ void enqueue();
228
+ }
229
+
230
+ // Process remaining files as slots open
231
+ while (pending.size > 0) {
232
+ await Promise.race(pending);
233
+ if (failed) {
234
+ await Promise.allSettled(pending);
235
+ throw new Error("Upload failed");
236
+ }
237
+ void enqueue();
238
+ }
239
+ }
240
+
241
+ function collectFiles(
242
+ dir: string,
243
+ baseDir: string,
244
+ ): Array<{ path: string; localPath: string; contentType: string }> {
245
+ const files: Array<{
246
+ path: string;
247
+ localPath: string;
248
+ contentType: string;
249
+ }> = [];
250
+
251
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
252
+ const fullPath = join(dir, entry.name);
253
+ if (entry.isDirectory()) {
254
+ files.push(...collectFiles(fullPath, baseDir));
255
+ } else if (entry.isFile()) {
256
+ files.push({
257
+ path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"),
258
+ localPath: fullPath,
259
+ contentType: getMimeType(fullPath),
260
+ });
261
+ }
262
+ }
263
+ return files;
264
+ }
265
+
266
+ /**
267
+ * Get the Convex site URL (.convex.site) from the cloud URL
268
+ */
269
+ function getConvexSiteUrl(prod: boolean): string | null {
270
+ try {
271
+ const envFlag = prod ? "--prod" : "";
272
+ const result = execSync(`npx convex env get CONVEX_CLOUD_URL ${envFlag}`, {
273
+ stdio: "pipe",
274
+ encoding: "utf-8",
275
+ });
276
+ const cloudUrl = result.trim();
277
+ if (cloudUrl && cloudUrl.includes(".convex.cloud")) {
278
+ return cloudUrl.replace(".convex.cloud", ".convex.site");
279
+ }
280
+ } catch {
281
+ // Ignore errors
282
+ }
283
+ return null;
284
+ }
285
+
286
+ export async function portalUploadMain(rawArgs: string[]): Promise<void> {
287
+ const args = parsePortalUploadArgs(rawArgs);
288
+
289
+ if (args.help) {
290
+ showHelp();
291
+ process.exit(0);
292
+ }
293
+
294
+ // Set global prod flag
295
+ useProd = args.prod;
296
+
297
+ // Run build if requested
298
+ if (args.build) {
299
+ let convexUrl: string | null = null;
300
+
301
+ if (useProd) {
302
+ try {
303
+ const result = execSync("npx convex dashboard --prod --no-open", {
304
+ stdio: "pipe",
305
+ encoding: "utf-8",
306
+ });
307
+ const match = result.match(/dashboard\.convex\.dev\/d\/([a-z0-9-]+)/i);
308
+ if (match) {
309
+ convexUrl = `https://${match[1]}.convex.cloud`;
310
+ }
311
+ } catch {
312
+ console.error("Could not get production Convex URL.");
313
+ console.error(
314
+ "Make sure you have deployed to production: npx convex deploy",
315
+ );
316
+ process.exit(1);
317
+ }
318
+ } else {
319
+ if (existsSync(".env.local")) {
320
+ const envContent = readFileSync(".env.local", "utf-8");
321
+ const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/);
322
+ if (match) {
323
+ convexUrl = match[1].trim();
324
+ }
325
+ }
326
+ }
327
+
328
+ if (!convexUrl) {
329
+ console.error("Could not determine Convex URL for build.");
330
+ process.exit(1);
331
+ }
332
+
333
+ const envLabel = useProd ? "production" : "development";
334
+ console.log(`Building for ${envLabel}...`);
335
+ console.log(` VITE_CONVEX_URL=${convexUrl}`);
336
+ console.log("");
337
+
338
+ const buildResult = spawnSync("npm", ["run", "build"], {
339
+ stdio: "inherit",
340
+ env: { ...process.env, VITE_CONVEX_URL: convexUrl },
341
+ });
342
+
343
+ if (buildResult.status !== 0) {
344
+ console.error("Build failed.");
345
+ process.exit(1);
346
+ }
347
+
348
+ console.log("");
349
+ }
350
+
351
+ const distDir = resolve(args.dist);
352
+ const componentName = args.component;
353
+
354
+ if (!existsSync(distDir)) {
355
+ console.error(`Error: dist directory not found: ${distDir}`);
356
+ console.error(
357
+ "Run your build command first (e.g., 'bun run build:portal' or add --build flag)",
358
+ );
359
+ process.exit(1);
360
+ }
361
+
362
+ const deploymentId = randomUUID();
363
+ const files = collectFiles(distDir, distDir);
364
+
365
+ const envLabel = useProd ? "production" : "development";
366
+ console.log(`Deploying portal to ${envLabel} environment`);
367
+ console.log(
368
+ `Uploading ${files.length} files with deployment ID: ${deploymentId}`,
369
+ );
370
+ console.log(`Component: ${componentName}`);
371
+ console.log("");
372
+
373
+ try {
374
+ await uploadWithConcurrency(
375
+ files,
376
+ componentName,
377
+ deploymentId,
378
+ args.concurrency,
379
+ );
380
+ } catch {
381
+ console.error("Upload failed.");
382
+ process.exit(1);
383
+ }
384
+
385
+ console.log("");
386
+
387
+ // Garbage collect old files
388
+ const gcOutput = await runHosting(componentName, "gcOldAssets", {
389
+ currentDeploymentId: deploymentId,
390
+ });
391
+ const gcResult = JSON.parse(gcOutput);
392
+
393
+ const deletedCount =
394
+ typeof gcResult === "number" ? gcResult : gcResult.deleted;
395
+
396
+ if (deletedCount > 0) {
397
+ console.log(
398
+ `Cleaned up ${deletedCount} old storage file(s) from previous deployments`,
399
+ );
400
+ }
401
+
402
+ console.log("");
403
+ console.log("Upload complete!");
404
+
405
+ // Show the deployment URL
406
+ const deployedSiteUrl = getConvexSiteUrl(useProd);
407
+ if (deployedSiteUrl) {
408
+ console.log("");
409
+ console.log(`Portal available at: ${deployedSiteUrl}/auth`);
410
+ }
411
+ }