@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.
- package/dist/bin.cjs +466 -63
- package/dist/client/index.d.ts +127 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +414 -0
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +56 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +93 -3
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/index.d.ts +5 -3
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +5 -3
- package/dist/component/index.js.map +1 -1
- package/dist/component/portalBridge.d.ts +80 -0
- package/dist/component/portalBridge.d.ts.map +1 -0
- package/dist/component/portalBridge.js +102 -0
- package/dist/component/portalBridge.js.map +1 -0
- package/dist/component/public.d.ts +193 -9
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +204 -33
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +89 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +68 -7
- package/dist/component/schema.js.map +1 -1
- package/dist/providers/passkey.d.ts +20 -0
- package/dist/providers/passkey.d.ts.map +1 -0
- package/dist/providers/passkey.js +32 -0
- package/dist/providers/passkey.js.map +1 -0
- package/dist/providers/totp.d.ts +14 -0
- package/dist/providers/totp.d.ts.map +1 -0
- package/dist/providers/totp.js +23 -0
- package/dist/providers/totp.js.map +1 -0
- package/dist/server/convex-auth.d.ts +243 -0
- package/dist/server/convex-auth.d.ts.map +1 -0
- package/dist/server/convex-auth.js +365 -0
- package/dist/server/convex-auth.js.map +1 -0
- package/dist/server/implementation/index.d.ts +80 -7
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +88 -5
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/passkey.d.ts +33 -0
- package/dist/server/implementation/passkey.d.ts.map +1 -0
- package/dist/server/implementation/passkey.js +450 -0
- package/dist/server/implementation/passkey.js.map +1 -0
- package/dist/server/implementation/redirects.d.ts.map +1 -1
- package/dist/server/implementation/redirects.js +4 -9
- package/dist/server/implementation/redirects.js.map +1 -1
- package/dist/server/implementation/signIn.d.ts +13 -0
- package/dist/server/implementation/signIn.d.ts.map +1 -1
- package/dist/server/implementation/signIn.js +26 -1
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/implementation/totp.d.ts +40 -0
- package/dist/server/implementation/totp.d.ts.map +1 -0
- package/dist/server/implementation/totp.js +211 -0
- package/dist/server/implementation/totp.js.map +1 -0
- package/dist/server/portal-email.d.ts +19 -0
- package/dist/server/portal-email.d.ts.map +1 -0
- package/dist/server/portal-email.js +89 -0
- package/dist/server/portal-email.js.map +1 -0
- package/dist/server/portal.d.ts +116 -0
- package/dist/server/portal.d.ts.map +1 -0
- package/dist/server/portal.js +294 -0
- package/dist/server/portal.js.map +1 -0
- package/dist/server/provider_utils.d.ts +1 -1
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/provider_utils.js +39 -1
- package/dist/server/provider_utils.js.map +1 -1
- package/dist/server/types.d.ts +58 -2
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/cli/index.ts +48 -6
- package/src/cli/portal-link.ts +112 -0
- package/src/cli/portal-upload.ts +411 -0
- package/src/client/index.ts +477 -0
- package/src/component/_generated/api.ts +72 -1
- package/src/component/_generated/component.ts +180 -4
- package/src/component/convex.config.ts +3 -0
- package/src/component/index.ts +5 -3
- package/src/component/portalBridge.ts +116 -0
- package/src/component/public.ts +231 -37
- package/src/component/schema.ts +70 -7
- package/src/providers/passkey.ts +35 -0
- package/src/providers/totp.ts +26 -0
- package/src/server/convex-auth.ts +470 -0
- package/src/server/implementation/index.ts +109 -8
- package/src/server/implementation/passkey.ts +650 -0
- package/src/server/implementation/redirects.ts +4 -11
- package/src/server/implementation/signIn.ts +39 -1
- package/src/server/implementation/totp.ts +366 -0
- package/src/server/portal-email.ts +95 -0
- package/src/server/portal.ts +375 -0
- package/src/server/provider_utils.ts +42 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
+
}
|