export-runtime 0.0.9 → 0.0.11
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/auth.js +129 -0
- package/bin/auth.mjs +313 -0
- package/bin/generate-types.mjs +162 -15
- package/client.js +256 -40
- package/entry.js +2 -1
- package/handler.js +250 -12
- package/package.json +13 -2
- package/rpc.js +2 -0
package/auth.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Better-auth integration for export-runtime
|
|
2
|
+
// This module provides authentication via better-auth with D1 storage
|
|
3
|
+
|
|
4
|
+
let betterAuthModule = null;
|
|
5
|
+
|
|
6
|
+
// Lazy load better-auth (optional peer dependency)
|
|
7
|
+
const loadBetterAuth = async () => {
|
|
8
|
+
if (betterAuthModule) return betterAuthModule;
|
|
9
|
+
try {
|
|
10
|
+
betterAuthModule = await import("better-auth");
|
|
11
|
+
return betterAuthModule;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Create auth instance for a request
|
|
18
|
+
export const createAuth = async (env, authConfig) => {
|
|
19
|
+
const { betterAuth } = await loadBetterAuth() || {};
|
|
20
|
+
if (!betterAuth) {
|
|
21
|
+
console.warn("[export-runtime] better-auth not installed. Run: npm install better-auth");
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { database, providers = {}, ...restConfig } = authConfig;
|
|
26
|
+
const dbBinding = database || "AUTH_DB";
|
|
27
|
+
const db = env[dbBinding];
|
|
28
|
+
|
|
29
|
+
if (!db) {
|
|
30
|
+
console.warn(`[export-runtime] D1 binding "${dbBinding}" not found for auth`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Build social providers config
|
|
35
|
+
const socialProviders = {};
|
|
36
|
+
|
|
37
|
+
if (providers.google) {
|
|
38
|
+
socialProviders.google = {
|
|
39
|
+
clientId: providers.google.clientId || env.GOOGLE_CLIENT_ID,
|
|
40
|
+
clientSecret: providers.google.clientSecret || env.GOOGLE_CLIENT_SECRET,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (providers.github) {
|
|
45
|
+
socialProviders.github = {
|
|
46
|
+
clientId: providers.github.clientId || env.GITHUB_CLIENT_ID,
|
|
47
|
+
clientSecret: providers.github.clientSecret || env.GITHUB_CLIENT_SECRET,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (providers.discord) {
|
|
52
|
+
socialProviders.discord = {
|
|
53
|
+
clientId: providers.discord.clientId || env.DISCORD_CLIENT_ID,
|
|
54
|
+
clientSecret: providers.discord.clientSecret || env.DISCORD_CLIENT_SECRET,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Initialize better-auth
|
|
59
|
+
const auth = betterAuth({
|
|
60
|
+
database: {
|
|
61
|
+
provider: "sqlite",
|
|
62
|
+
url: db, // D1 is SQLite-compatible
|
|
63
|
+
},
|
|
64
|
+
secret: env.BETTER_AUTH_SECRET || env.AUTH_SECRET,
|
|
65
|
+
emailAndPassword: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
},
|
|
68
|
+
socialProviders,
|
|
69
|
+
session: {
|
|
70
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
71
|
+
updateAge: 60 * 60 * 24, // Refresh after 1 day
|
|
72
|
+
cookieCache: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
maxAge: 5 * 60, // 5 minutes
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
...restConfig,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return auth;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Handle auth HTTP routes
|
|
84
|
+
export const handleAuthRoute = async (request, env, authConfig) => {
|
|
85
|
+
const auth = await createAuth(env, authConfig);
|
|
86
|
+
if (!auth) {
|
|
87
|
+
return new Response(JSON.stringify({ error: "Auth not configured" }), {
|
|
88
|
+
status: 500,
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Let better-auth handle the request
|
|
94
|
+
return auth.handler(request);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Get session from request headers (for WebSocket auth)
|
|
98
|
+
export const getSessionFromRequest = async (request, env, authConfig) => {
|
|
99
|
+
const auth = await createAuth(env, authConfig);
|
|
100
|
+
if (!auth) return null;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const session = await auth.api.getSession({
|
|
104
|
+
headers: request.headers,
|
|
105
|
+
});
|
|
106
|
+
return session;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Verify session token (for WebSocket messages)
|
|
113
|
+
export const verifySession = async (token, env, authConfig) => {
|
|
114
|
+
const auth = await createAuth(env, authConfig);
|
|
115
|
+
if (!auth) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Create a mock request with the session cookie
|
|
119
|
+
const mockHeaders = new Headers();
|
|
120
|
+
mockHeaders.set("Cookie", `better-auth.session_token=${token}`);
|
|
121
|
+
|
|
122
|
+
const session = await auth.api.getSession({
|
|
123
|
+
headers: mockHeaders,
|
|
124
|
+
});
|
|
125
|
+
return session;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
};
|
package/bin/auth.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
8
|
+
|
|
9
|
+
// Supported OAuth providers and their required env vars
|
|
10
|
+
const PROVIDERS = {
|
|
11
|
+
google: {
|
|
12
|
+
clientId: "GOOGLE_CLIENT_ID",
|
|
13
|
+
clientSecret: "GOOGLE_CLIENT_SECRET",
|
|
14
|
+
},
|
|
15
|
+
github: {
|
|
16
|
+
clientId: "GITHUB_CLIENT_ID",
|
|
17
|
+
clientSecret: "GITHUB_CLIENT_SECRET",
|
|
18
|
+
},
|
|
19
|
+
discord: {
|
|
20
|
+
clientId: "DISCORD_CLIENT_ID",
|
|
21
|
+
clientSecret: "DISCORD_CLIENT_SECRET",
|
|
22
|
+
},
|
|
23
|
+
twitter: {
|
|
24
|
+
clientId: "TWITTER_CLIENT_ID",
|
|
25
|
+
clientSecret: "TWITTER_CLIENT_SECRET",
|
|
26
|
+
},
|
|
27
|
+
facebook: {
|
|
28
|
+
clientId: "FACEBOOK_CLIENT_ID",
|
|
29
|
+
clientSecret: "FACEBOOK_CLIENT_SECRET",
|
|
30
|
+
},
|
|
31
|
+
apple: {
|
|
32
|
+
clientId: "APPLE_CLIENT_ID",
|
|
33
|
+
clientSecret: "APPLE_CLIENT_SECRET",
|
|
34
|
+
},
|
|
35
|
+
microsoft: {
|
|
36
|
+
clientId: "MICROSOFT_CLIENT_ID",
|
|
37
|
+
clientSecret: "MICROSOFT_CLIENT_SECRET",
|
|
38
|
+
},
|
|
39
|
+
linkedin: {
|
|
40
|
+
clientId: "LINKEDIN_CLIENT_ID",
|
|
41
|
+
clientSecret: "LINKEDIN_CLIENT_SECRET",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function readPackageJson() {
|
|
46
|
+
if (!fs.existsSync(pkgPath)) {
|
|
47
|
+
console.error("package.json not found in", cwd);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writePackageJson(pkg) {
|
|
54
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readEnvFile() {
|
|
58
|
+
const envPath = path.join(cwd, ".dev.vars");
|
|
59
|
+
if (!fs.existsSync(envPath)) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
63
|
+
const env = {};
|
|
64
|
+
for (const line of content.split("\n")) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
67
|
+
const eqIndex = trimmed.indexOf("=");
|
|
68
|
+
if (eqIndex > 0) {
|
|
69
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
70
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
71
|
+
// Remove quotes if present
|
|
72
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
73
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
74
|
+
value = value.slice(1, -1);
|
|
75
|
+
}
|
|
76
|
+
env[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeEnvFile(env) {
|
|
83
|
+
const envPath = path.join(cwd, ".dev.vars");
|
|
84
|
+
const lines = [];
|
|
85
|
+
|
|
86
|
+
// Add header comment if file is new
|
|
87
|
+
if (!fs.existsSync(envPath)) {
|
|
88
|
+
lines.push("# Local development environment variables");
|
|
89
|
+
lines.push("# These are used by wrangler dev and should NOT be committed to git");
|
|
90
|
+
lines.push("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const [key, value] of Object.entries(env)) {
|
|
94
|
+
// Quote values that contain spaces or special characters
|
|
95
|
+
const needsQuotes = /[\s"'=]/.test(value);
|
|
96
|
+
lines.push(`${key}=${needsQuotes ? `"${value}"` : value}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(envPath, lines.join("\n") + "\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureGitignore() {
|
|
103
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
104
|
+
let content = "";
|
|
105
|
+
if (fs.existsSync(gitignorePath)) {
|
|
106
|
+
content = fs.readFileSync(gitignorePath, "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!content.includes(".dev.vars")) {
|
|
110
|
+
const lines = content ? content.split("\n") : [];
|
|
111
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
lines.push("# Local secrets (wrangler dev)");
|
|
115
|
+
lines.push(".dev.vars");
|
|
116
|
+
lines.push("");
|
|
117
|
+
fs.writeFileSync(gitignorePath, lines.join("\n"));
|
|
118
|
+
console.log("Added .dev.vars to .gitignore");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function addProvider(provider, credentials) {
|
|
123
|
+
const providerLower = provider.toLowerCase();
|
|
124
|
+
|
|
125
|
+
if (!PROVIDERS[providerLower]) {
|
|
126
|
+
console.error(`Unknown provider: ${provider}`);
|
|
127
|
+
console.error("Supported providers:", Object.keys(PROVIDERS).join(", "));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse credentials (format: clientId:clientSecret or just clientId if secret provided separately)
|
|
132
|
+
let clientId, clientSecret;
|
|
133
|
+
if (credentials.includes(":")) {
|
|
134
|
+
[clientId, clientSecret] = credentials.split(":", 2);
|
|
135
|
+
} else {
|
|
136
|
+
clientId = credentials;
|
|
137
|
+
clientSecret = process.argv[5]; // Fourth argument
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!clientId || !clientSecret) {
|
|
141
|
+
console.error("Usage: npm run auth:add -- <provider> <clientId>:<clientSecret>");
|
|
142
|
+
console.error(" or: npm run auth:add -- <provider> <clientId> <clientSecret>");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Update package.json to enable auth
|
|
147
|
+
const pkg = readPackageJson();
|
|
148
|
+
if (!pkg.cloudflare) pkg.cloudflare = {};
|
|
149
|
+
if (!pkg.cloudflare.auth) pkg.cloudflare.auth = {};
|
|
150
|
+
if (pkg.cloudflare.auth === true) pkg.cloudflare.auth = {};
|
|
151
|
+
|
|
152
|
+
// Add provider to auth config
|
|
153
|
+
if (!pkg.cloudflare.auth.providers) pkg.cloudflare.auth.providers = [];
|
|
154
|
+
if (!pkg.cloudflare.auth.providers.includes(providerLower)) {
|
|
155
|
+
pkg.cloudflare.auth.providers.push(providerLower);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
writePackageJson(pkg);
|
|
159
|
+
console.log(`Enabled ${provider} authentication in package.json`);
|
|
160
|
+
|
|
161
|
+
// Update .dev.vars with credentials
|
|
162
|
+
const env = readEnvFile();
|
|
163
|
+
const providerConfig = PROVIDERS[providerLower];
|
|
164
|
+
env[providerConfig.clientId] = clientId;
|
|
165
|
+
env[providerConfig.clientSecret] = clientSecret;
|
|
166
|
+
|
|
167
|
+
// Ensure BETTER_AUTH_SECRET exists
|
|
168
|
+
if (!env.BETTER_AUTH_SECRET) {
|
|
169
|
+
env.BETTER_AUTH_SECRET = generateSecret();
|
|
170
|
+
console.log("Generated BETTER_AUTH_SECRET");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
writeEnvFile(env);
|
|
174
|
+
ensureGitignore();
|
|
175
|
+
|
|
176
|
+
console.log(`Saved ${provider} credentials to .dev.vars`);
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log("For production, set these secrets in Cloudflare dashboard:");
|
|
179
|
+
console.log(` ${providerConfig.clientId}=${clientId}`);
|
|
180
|
+
console.log(` ${providerConfig.clientSecret}=***`);
|
|
181
|
+
console.log(` BETTER_AUTH_SECRET=***`);
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log("OAuth callback URL:");
|
|
184
|
+
console.log(` https://your-worker.workers.dev/api/auth/callback/${providerLower}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function removeProvider(provider) {
|
|
188
|
+
const providerLower = provider.toLowerCase();
|
|
189
|
+
|
|
190
|
+
if (!PROVIDERS[providerLower]) {
|
|
191
|
+
console.error(`Unknown provider: ${provider}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Update package.json
|
|
196
|
+
const pkg = readPackageJson();
|
|
197
|
+
if (pkg.cloudflare?.auth?.providers) {
|
|
198
|
+
pkg.cloudflare.auth.providers = pkg.cloudflare.auth.providers.filter(p => p !== providerLower);
|
|
199
|
+
if (pkg.cloudflare.auth.providers.length === 0) {
|
|
200
|
+
delete pkg.cloudflare.auth.providers;
|
|
201
|
+
}
|
|
202
|
+
// If auth config is empty object, set to true for simpler config
|
|
203
|
+
if (Object.keys(pkg.cloudflare.auth).length === 0) {
|
|
204
|
+
pkg.cloudflare.auth = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
writePackageJson(pkg);
|
|
209
|
+
console.log(`Removed ${provider} from package.json`);
|
|
210
|
+
|
|
211
|
+
// Remove from .dev.vars
|
|
212
|
+
const env = readEnvFile();
|
|
213
|
+
const providerConfig = PROVIDERS[providerLower];
|
|
214
|
+
delete env[providerConfig.clientId];
|
|
215
|
+
delete env[providerConfig.clientSecret];
|
|
216
|
+
|
|
217
|
+
writeEnvFile(env);
|
|
218
|
+
console.log(`Removed ${provider} credentials from .dev.vars`);
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log("Remember to also remove the secrets from Cloudflare dashboard");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function listProviders() {
|
|
224
|
+
const pkg = readPackageJson();
|
|
225
|
+
const env = readEnvFile();
|
|
226
|
+
|
|
227
|
+
console.log("Available OAuth providers:");
|
|
228
|
+
console.log("");
|
|
229
|
+
|
|
230
|
+
for (const [name, config] of Object.entries(PROVIDERS)) {
|
|
231
|
+
const hasCredentials = env[config.clientId] && env[config.clientSecret];
|
|
232
|
+
const isEnabled = pkg.cloudflare?.auth?.providers?.includes(name);
|
|
233
|
+
const status = isEnabled && hasCredentials ? "[configured]" :
|
|
234
|
+
isEnabled ? "[enabled, missing credentials]" :
|
|
235
|
+
hasCredentials ? "[credentials only]" : "";
|
|
236
|
+
console.log(` ${name} ${status}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log("");
|
|
240
|
+
if (pkg.cloudflare?.auth) {
|
|
241
|
+
console.log("Auth is enabled in package.json");
|
|
242
|
+
} else {
|
|
243
|
+
console.log("Auth is not enabled. Run: npm run auth:add -- <provider> <credentials>");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function generateSecret() {
|
|
248
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
249
|
+
let result = "";
|
|
250
|
+
const randomValues = new Uint8Array(32);
|
|
251
|
+
crypto.getRandomValues(randomValues);
|
|
252
|
+
for (let i = 0; i < 32; i++) {
|
|
253
|
+
result += chars[randomValues[i] % chars.length];
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function showHelp() {
|
|
259
|
+
console.log(`
|
|
260
|
+
export-auth - Manage OAuth providers for export-runtime
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
npm run auth:add -- <provider> <clientId>:<clientSecret>
|
|
264
|
+
npm run auth:add -- <provider> <clientId> <clientSecret>
|
|
265
|
+
npm run auth:remove -- <provider>
|
|
266
|
+
npm run auth:list
|
|
267
|
+
|
|
268
|
+
Commands:
|
|
269
|
+
add Add an OAuth provider with credentials
|
|
270
|
+
remove Remove an OAuth provider
|
|
271
|
+
list List available providers and their status
|
|
272
|
+
|
|
273
|
+
Supported providers:
|
|
274
|
+
${Object.keys(PROVIDERS).join(", ")}
|
|
275
|
+
|
|
276
|
+
Examples:
|
|
277
|
+
npm run auth:add -- google 123456.apps.googleusercontent.com:GOCSPX-xxx
|
|
278
|
+
npm run auth:add -- github Iv1.abc123:ghp_xxx
|
|
279
|
+
npm run auth:remove -- google
|
|
280
|
+
npm run auth:list
|
|
281
|
+
`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Main
|
|
285
|
+
const [,, command, ...args] = process.argv;
|
|
286
|
+
|
|
287
|
+
switch (command) {
|
|
288
|
+
case "add":
|
|
289
|
+
if (args.length < 2) {
|
|
290
|
+
console.error("Usage: npm run auth:add -- <provider> <clientId>:<clientSecret>");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
addProvider(args[0], args[1]);
|
|
294
|
+
break;
|
|
295
|
+
case "remove":
|
|
296
|
+
if (args.length < 1) {
|
|
297
|
+
console.error("Usage: npm run auth:remove -- <provider>");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
removeProvider(args[0]);
|
|
301
|
+
break;
|
|
302
|
+
case "list":
|
|
303
|
+
listProviders();
|
|
304
|
+
break;
|
|
305
|
+
case "help":
|
|
306
|
+
case "--help":
|
|
307
|
+
case "-h":
|
|
308
|
+
showHelp();
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
showHelp();
|
|
312
|
+
process.exit(command ? 1 : 0);
|
|
313
|
+
}
|
package/bin/generate-types.mjs
CHANGED
|
@@ -9,25 +9,69 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
|
|
10
10
|
const cwd = process.cwd();
|
|
11
11
|
|
|
12
|
-
// Read
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// --- Read package.json for configuration ---
|
|
13
|
+
|
|
14
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
15
|
+
if (!fs.existsSync(pkgPath)) {
|
|
16
|
+
console.error("package.json not found in", cwd);
|
|
16
17
|
process.exit(1);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
21
|
+
|
|
22
|
+
// Required fields
|
|
23
|
+
const workerName = pkg.name;
|
|
24
|
+
if (!workerName) {
|
|
25
|
+
console.error("package.json must have a 'name' field for the Worker name");
|
|
22
26
|
process.exit(1);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const exportsEntry = pkg.exports;
|
|
30
|
+
if (!exportsEntry) {
|
|
31
|
+
console.error("package.json must have an 'exports' field pointing to the source entry (e.g., './src' or './src/index.ts')");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Optional: static assets directory
|
|
36
|
+
const assetsDir = pkg.main || null;
|
|
37
|
+
|
|
38
|
+
// Optional: Cloudflare bindings configuration (d1, r2, kv, auth)
|
|
39
|
+
const cloudflareConfig = pkg.cloudflare || {};
|
|
40
|
+
const d1Bindings = cloudflareConfig.d1 || [];
|
|
41
|
+
const r2Bindings = cloudflareConfig.r2 || [];
|
|
42
|
+
const kvBindings = cloudflareConfig.kv || [];
|
|
43
|
+
const authConfig = cloudflareConfig.auth || null;
|
|
44
|
+
|
|
45
|
+
// Auth requires a D1 database for better-auth
|
|
46
|
+
const allD1Bindings = [...d1Bindings];
|
|
47
|
+
if (authConfig && !allD1Bindings.includes("AUTH_DB")) {
|
|
48
|
+
allD1Bindings.unshift("AUTH_DB");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate binding names
|
|
52
|
+
const validateBindings = (bindings, type) => {
|
|
53
|
+
for (const name of bindings) {
|
|
54
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
55
|
+
console.error(`Invalid ${type} binding name: "${name}". Use UPPER_SNAKE_CASE.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
validateBindings(d1Bindings, "D1");
|
|
61
|
+
validateBindings(r2Bindings, "R2");
|
|
62
|
+
validateBindings(kvBindings, "KV");
|
|
63
|
+
|
|
64
|
+
// --- Resolve source directory from exports field ---
|
|
65
|
+
|
|
66
|
+
const exportsPath = path.resolve(cwd, exportsEntry.replace(/^\.\//, ""));
|
|
67
|
+
const srcDir = fs.existsSync(exportsPath) && fs.statSync(exportsPath).isDirectory()
|
|
68
|
+
? exportsPath
|
|
69
|
+
: path.dirname(exportsPath);
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(srcDir)) {
|
|
72
|
+
console.error(`Source directory not found: ${srcDir}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
31
75
|
|
|
32
76
|
// --- Discover all source files under srcDir ---
|
|
33
77
|
|
|
@@ -242,7 +286,17 @@ for (const mod of modules) {
|
|
|
242
286
|
// --- Minify core modules ---
|
|
243
287
|
|
|
244
288
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
245
|
-
const {
|
|
289
|
+
const { generateCoreCode } = await import(path.join(__dirname, "..", "client.js"));
|
|
290
|
+
|
|
291
|
+
// Generate core code with export config
|
|
292
|
+
const coreConfig = {
|
|
293
|
+
d1: allD1Bindings,
|
|
294
|
+
r2: r2Bindings,
|
|
295
|
+
kv: kvBindings,
|
|
296
|
+
auth: !!authConfig,
|
|
297
|
+
};
|
|
298
|
+
const CORE_CODE = generateCoreCode(coreConfig);
|
|
299
|
+
const SHARED_CORE_CODE = generateCoreCode({ ...coreConfig, shared: true });
|
|
246
300
|
|
|
247
301
|
const minified = minifySync("_core.js", CORE_CODE);
|
|
248
302
|
if (minified.errors?.length) console.error("Minification errors (core):", minified.errors);
|
|
@@ -327,7 +381,100 @@ for (const mod of modules) {
|
|
|
327
381
|
sharedLines.push(`export { getStub };`);
|
|
328
382
|
fs.writeFileSync(sharedModulePath, sharedLines.join("\n") + "\n");
|
|
329
383
|
|
|
384
|
+
// --- Generate wrangler.toml ---
|
|
385
|
+
|
|
386
|
+
const wranglerLines = [
|
|
387
|
+
`# Auto-generated by export-runtime. Do not edit manually.`,
|
|
388
|
+
`name = "${workerName}"`,
|
|
389
|
+
`main = "node_modules/export-runtime/entry.js"`,
|
|
390
|
+
`compatibility_date = "2024-11-01"`,
|
|
391
|
+
``,
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
// Add static assets configuration if main is specified
|
|
395
|
+
if (assetsDir) {
|
|
396
|
+
const normalizedAssetsDir = assetsDir.startsWith("./") ? assetsDir : `./${assetsDir}`;
|
|
397
|
+
wranglerLines.push(
|
|
398
|
+
`[assets]`,
|
|
399
|
+
`directory = "${normalizedAssetsDir}"`,
|
|
400
|
+
`binding = "ASSETS"`,
|
|
401
|
+
`run_worker_first = true`,
|
|
402
|
+
``,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Add Durable Objects for shared state
|
|
407
|
+
wranglerLines.push(
|
|
408
|
+
`[durable_objects]`,
|
|
409
|
+
`bindings = [`,
|
|
410
|
+
` { name = "SHARED_EXPORT", class_name = "SharedExportDO" }`,
|
|
411
|
+
`]`,
|
|
412
|
+
``,
|
|
413
|
+
`[[migrations]]`,
|
|
414
|
+
`tag = "v1"`,
|
|
415
|
+
`new_classes = ["SharedExportDO"]`,
|
|
416
|
+
``,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Add D1 bindings
|
|
420
|
+
if (allD1Bindings.length > 0) {
|
|
421
|
+
for (const name of allD1Bindings) {
|
|
422
|
+
wranglerLines.push(`[[d1_databases]]`);
|
|
423
|
+
wranglerLines.push(`binding = "${name}"`);
|
|
424
|
+
wranglerLines.push(`database_name = "${workerName}-${name.toLowerCase().replace(/_/g, "-")}"`);
|
|
425
|
+
wranglerLines.push(`database_id = "" # Run: wrangler d1 create ${workerName}-${name.toLowerCase().replace(/_/g, "-")}`);
|
|
426
|
+
wranglerLines.push(``);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Add R2 bindings
|
|
431
|
+
if (r2Bindings.length > 0) {
|
|
432
|
+
for (const name of r2Bindings) {
|
|
433
|
+
wranglerLines.push(`[[r2_buckets]]`);
|
|
434
|
+
wranglerLines.push(`binding = "${name}"`);
|
|
435
|
+
wranglerLines.push(`bucket_name = "${workerName}-${name.toLowerCase().replace(/_/g, "-")}"`);
|
|
436
|
+
wranglerLines.push(``);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Add KV bindings
|
|
441
|
+
if (kvBindings.length > 0) {
|
|
442
|
+
for (const name of kvBindings) {
|
|
443
|
+
wranglerLines.push(`[[kv_namespaces]]`);
|
|
444
|
+
wranglerLines.push(`binding = "${name}"`);
|
|
445
|
+
wranglerLines.push(`id = "" # Run: wrangler kv namespace create ${name}`);
|
|
446
|
+
wranglerLines.push(``);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Add alias
|
|
451
|
+
wranglerLines.push(
|
|
452
|
+
`[alias]`,
|
|
453
|
+
`"__USER_MODULE__" = "./.export-module-map.js"`,
|
|
454
|
+
`"__GENERATED_TYPES__" = "./.export-types.js"`,
|
|
455
|
+
`"__SHARED_MODULE__" = "./.export-shared.js"`,
|
|
456
|
+
`"__EXPORT_CONFIG__" = "./.export-config.js"`,
|
|
457
|
+
``,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// --- Write .export-config.js ---
|
|
461
|
+
|
|
462
|
+
const configPath = path.join(cwd, ".export-config.js");
|
|
463
|
+
const configContent = `// Auto-generated export configuration
|
|
464
|
+
export const d1Bindings = ${JSON.stringify(allD1Bindings)};
|
|
465
|
+
export const r2Bindings = ${JSON.stringify(r2Bindings)};
|
|
466
|
+
export const kvBindings = ${JSON.stringify(kvBindings)};
|
|
467
|
+
export const authConfig = ${JSON.stringify(authConfig)};
|
|
468
|
+
`;
|
|
469
|
+
fs.writeFileSync(configPath, configContent);
|
|
470
|
+
|
|
471
|
+
const wranglerPath = path.join(cwd, "wrangler.toml");
|
|
472
|
+
fs.writeFileSync(wranglerPath, wranglerLines.join("\n"));
|
|
473
|
+
|
|
474
|
+
// --- Output summary ---
|
|
475
|
+
|
|
330
476
|
console.log(`Discovered ${modules.length} module(s): ${modules.map(m => m.routePath || "/").join(", ")}`);
|
|
331
477
|
console.log("Generated type definitions + minified core →", outPath);
|
|
332
478
|
console.log("Generated module map →", moduleMapPath);
|
|
333
479
|
console.log("Generated shared import module →", sharedModulePath);
|
|
480
|
+
console.log("Generated wrangler.toml →", wranglerPath);
|
package/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Core module template. __WS_SUFFIX__ is replaced: "./" for normal, "./?shared" for shared.
|
|
2
|
+
// __D1_BINDINGS__, __R2_BINDINGS__, __KV_BINDINGS__, __AUTH_ENABLED__ are replaced with config.
|
|
2
3
|
const CORE_TEMPLATE = `
|
|
3
4
|
const stringify = (value) => {
|
|
4
5
|
const stringified = [];
|
|
@@ -109,22 +110,77 @@ const parse = (serialized) => {
|
|
|
109
110
|
|
|
110
111
|
const _u = new URL("__WS_SUFFIX__", import.meta.url);
|
|
111
112
|
_u.protocol = _u.protocol === "https:" ? "wss:" : "ws:";
|
|
112
|
-
|
|
113
|
+
let ws = new WebSocket(_u.href);
|
|
113
114
|
const pending = new Map();
|
|
114
115
|
let nextId = 1;
|
|
115
116
|
let keepaliveInterval;
|
|
117
|
+
let sessionToken = null;
|
|
116
118
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
+
const setupWs = (socket) => {
|
|
120
|
+
socket.onopen = () => {
|
|
119
121
|
keepaliveInterval = setInterval(() => {
|
|
120
|
-
if (
|
|
122
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(stringify({ type: "ping", id: 0 }));
|
|
121
123
|
}, 30000);
|
|
122
|
-
|
|
124
|
+
// Send session token if available
|
|
125
|
+
if (sessionToken) {
|
|
126
|
+
socket.send(stringify({ type: "auth", token: sessionToken, id: 0 }));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
socket.onclose = () => { clearInterval(keepaliveInterval); };
|
|
130
|
+
socket.onmessage = (event) => {
|
|
131
|
+
const msg = parse(event.data);
|
|
132
|
+
|
|
133
|
+
// Handle auth response
|
|
134
|
+
if (msg.type === "auth-result") {
|
|
135
|
+
if (msg.token) sessionToken = msg.token;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resolver = pending.get(msg.id);
|
|
140
|
+
if (!resolver) return;
|
|
141
|
+
pending.delete(msg.id);
|
|
142
|
+
|
|
143
|
+
if (msg.type === "error") {
|
|
144
|
+
resolver.reject(new Error(msg.error));
|
|
145
|
+
} else if (msg.type === "result") {
|
|
146
|
+
if (msg.valueType === "function") resolver.resolve(createProxy(msg.path));
|
|
147
|
+
else if (msg.valueType === "instance") resolver.resolve(createInstanceProxy(msg.instanceId));
|
|
148
|
+
else if (msg.valueType === "asynciterator") resolver.resolve({
|
|
149
|
+
[Symbol.asyncIterator]() { return this; },
|
|
150
|
+
next: () => sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId }),
|
|
151
|
+
return: () => sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId })
|
|
152
|
+
});
|
|
153
|
+
else if (msg.valueType === "readablestream") resolver.resolve(new ReadableStream({
|
|
154
|
+
async pull(c) {
|
|
155
|
+
try { const r = await sendRequest({ type: "stream-read", streamId: msg.streamId }); r.done ? c.close() : c.enqueue(r.value); }
|
|
156
|
+
catch (e) { c.error(e); }
|
|
157
|
+
},
|
|
158
|
+
cancel: () => sendRequest({ type: "stream-cancel", streamId: msg.streamId })
|
|
159
|
+
}));
|
|
160
|
+
else resolver.resolve(msg.value);
|
|
161
|
+
} else if (msg.type === "iterate-result") {
|
|
162
|
+
resolver.resolve({ value: msg.value, done: msg.done });
|
|
163
|
+
} else if (msg.type === "stream-result") {
|
|
164
|
+
resolver.resolve({ value: Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value, done: msg.done });
|
|
165
|
+
}
|
|
123
166
|
};
|
|
124
|
-
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
setupWs(ws);
|
|
170
|
+
|
|
171
|
+
const ready = new Promise((resolve, reject) => {
|
|
172
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
173
|
+
ws.addEventListener("error", reject, { once: true });
|
|
125
174
|
});
|
|
126
175
|
|
|
127
|
-
|
|
176
|
+
const reconnect = () => {
|
|
177
|
+
if (ws.readyState === WebSocket.OPEN) ws.close();
|
|
178
|
+
ws = new WebSocket(_u.href);
|
|
179
|
+
setupWs(ws);
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
ws.addEventListener("open", () => resolve(), { once: true });
|
|
182
|
+
});
|
|
183
|
+
};
|
|
128
184
|
|
|
129
185
|
const sendRequest = async (msg) => {
|
|
130
186
|
await ready;
|
|
@@ -135,37 +191,6 @@ const sendRequest = async (msg) => {
|
|
|
135
191
|
});
|
|
136
192
|
};
|
|
137
193
|
|
|
138
|
-
ws.onmessage = (event) => {
|
|
139
|
-
const msg = parse(event.data);
|
|
140
|
-
const resolver = pending.get(msg.id);
|
|
141
|
-
if (!resolver) return;
|
|
142
|
-
pending.delete(msg.id);
|
|
143
|
-
|
|
144
|
-
if (msg.type === "error") {
|
|
145
|
-
resolver.reject(new Error(msg.error));
|
|
146
|
-
} else if (msg.type === "result") {
|
|
147
|
-
if (msg.valueType === "function") resolver.resolve(createProxy(msg.path));
|
|
148
|
-
else if (msg.valueType === "instance") resolver.resolve(createInstanceProxy(msg.instanceId));
|
|
149
|
-
else if (msg.valueType === "asynciterator") resolver.resolve({
|
|
150
|
-
[Symbol.asyncIterator]() { return this; },
|
|
151
|
-
next: () => sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId }),
|
|
152
|
-
return: () => sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId })
|
|
153
|
-
});
|
|
154
|
-
else if (msg.valueType === "readablestream") resolver.resolve(new ReadableStream({
|
|
155
|
-
async pull(c) {
|
|
156
|
-
try { const r = await sendRequest({ type: "stream-read", streamId: msg.streamId }); r.done ? c.close() : c.enqueue(r.value); }
|
|
157
|
-
catch (e) { c.error(e); }
|
|
158
|
-
},
|
|
159
|
-
cancel: () => sendRequest({ type: "stream-cancel", streamId: msg.streamId })
|
|
160
|
-
}));
|
|
161
|
-
else resolver.resolve(msg.value);
|
|
162
|
-
} else if (msg.type === "iterate-result") {
|
|
163
|
-
resolver.resolve({ value: msg.value, done: msg.done });
|
|
164
|
-
} else if (msg.type === "stream-result") {
|
|
165
|
-
resolver.resolve({ value: Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value, done: msg.done });
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
194
|
const createInstanceProxy = (instanceId, path = []) => new Proxy(function(){}, {
|
|
170
195
|
get(_, prop) {
|
|
171
196
|
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
@@ -190,7 +215,198 @@ export const createProxy = (path = []) => new Proxy(function(){}, {
|
|
|
190
215
|
async apply(_, __, args) { return sendRequest({ type: "call", path, args }); },
|
|
191
216
|
construct(_, args) { return sendRequest({ type: "construct", path, args }); }
|
|
192
217
|
});
|
|
218
|
+
|
|
219
|
+
// --- Client object (default export) ---
|
|
220
|
+
|
|
221
|
+
// D1 query builder with template literal support
|
|
222
|
+
const createD1Query = (binding, sql, params) => {
|
|
223
|
+
const query = {
|
|
224
|
+
sql,
|
|
225
|
+
params,
|
|
226
|
+
then(resolve, reject) {
|
|
227
|
+
// Default to .all()
|
|
228
|
+
return sendRequest({ type: "d1", binding, method: "all", sql, params }).then(resolve, reject);
|
|
229
|
+
},
|
|
230
|
+
all() {
|
|
231
|
+
return sendRequest({ type: "d1", binding, method: "all", sql, params });
|
|
232
|
+
},
|
|
233
|
+
first(colName) {
|
|
234
|
+
return sendRequest({ type: "d1", binding, method: "first", sql, params, colName });
|
|
235
|
+
},
|
|
236
|
+
run() {
|
|
237
|
+
return sendRequest({ type: "d1", binding, method: "run", sql, params });
|
|
238
|
+
},
|
|
239
|
+
raw() {
|
|
240
|
+
return sendRequest({ type: "d1", binding, method: "raw", sql, params });
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return query;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// D1 tagged template literal handler
|
|
247
|
+
const createD1Proxy = (binding) => {
|
|
248
|
+
return (strings, ...values) => {
|
|
249
|
+
// Build parameterized SQL
|
|
250
|
+
let sql = strings[0];
|
|
251
|
+
for (let i = 0; i < values.length; i++) {
|
|
252
|
+
sql += "?" + strings[i + 1];
|
|
253
|
+
}
|
|
254
|
+
return createD1Query(binding, sql, values);
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// R2 bucket proxy
|
|
259
|
+
const createR2Proxy = (binding) => ({
|
|
260
|
+
get: (key, options) => sendRequest({ type: "r2", binding, method: "get", key, options }),
|
|
261
|
+
put: (key, value, options) => sendRequest({ type: "r2", binding, method: "put", key, value, options }),
|
|
262
|
+
delete: (key) => sendRequest({ type: "r2", binding, method: "delete", key }),
|
|
263
|
+
list: (options) => sendRequest({ type: "r2", binding, method: "list", options }),
|
|
264
|
+
head: (key) => sendRequest({ type: "r2", binding, method: "head", key }),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// KV namespace proxy
|
|
268
|
+
const createKVProxy = (binding) => ({
|
|
269
|
+
get: (key, options) => sendRequest({ type: "kv", binding, method: "get", key, options }),
|
|
270
|
+
put: (key, value, options) => sendRequest({ type: "kv", binding, method: "put", key, value, options }),
|
|
271
|
+
delete: (key) => sendRequest({ type: "kv", binding, method: "delete", key }),
|
|
272
|
+
list: (options) => sendRequest({ type: "kv", binding, method: "list", options }),
|
|
273
|
+
getWithMetadata: (key, options) => sendRequest({ type: "kv", binding, method: "getWithMetadata", key, options }),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Auth client proxy
|
|
277
|
+
const createAuthProxy = () => {
|
|
278
|
+
// Check for session token in URL hash (after OAuth redirect)
|
|
279
|
+
const checkUrlToken = () => {
|
|
280
|
+
if (typeof window !== "undefined" && window.location.hash) {
|
|
281
|
+
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
282
|
+
const token = params.get("token");
|
|
283
|
+
if (token) {
|
|
284
|
+
sessionToken = token;
|
|
285
|
+
// Clean up URL
|
|
286
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
287
|
+
return token;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Try to restore session on init
|
|
294
|
+
checkUrlToken();
|
|
295
|
+
|
|
296
|
+
const signIn = {
|
|
297
|
+
social: async (provider, options = {}) => {
|
|
298
|
+
const result = await sendRequest({ type: "auth", method: "signIn.social", provider, options });
|
|
299
|
+
if (result.redirectUrl) {
|
|
300
|
+
// Redirect to OAuth provider
|
|
301
|
+
if (typeof window !== "undefined") {
|
|
302
|
+
window.location.href = result.redirectUrl;
|
|
303
|
+
// Return a promise that never resolves (page is redirecting)
|
|
304
|
+
return new Promise(() => {});
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
if (result.token) {
|
|
309
|
+
sessionToken = result.token;
|
|
310
|
+
await reconnect();
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
},
|
|
314
|
+
email: async (email, password, options) => {
|
|
315
|
+
const result = await sendRequest({ type: "auth", method: "signIn.email", email, password, options });
|
|
316
|
+
if (result.success && result.token) {
|
|
317
|
+
sessionToken = result.token;
|
|
318
|
+
await reconnect();
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const signUp = {
|
|
325
|
+
email: async (email, password, name, options) => {
|
|
326
|
+
const result = await sendRequest({ type: "auth", method: "signUp.email", email, password, name, options });
|
|
327
|
+
if (result.success && result.token) {
|
|
328
|
+
sessionToken = result.token;
|
|
329
|
+
await reconnect();
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
signIn,
|
|
337
|
+
signUp,
|
|
338
|
+
signOut: async () => {
|
|
339
|
+
const result = await sendRequest({ type: "auth", method: "signOut" });
|
|
340
|
+
sessionToken = null;
|
|
341
|
+
await reconnect();
|
|
342
|
+
return result;
|
|
343
|
+
},
|
|
344
|
+
getSession: () => sendRequest({ type: "auth", method: "getSession" }),
|
|
345
|
+
getUser: () => sendRequest({ type: "auth", method: "getUser" }),
|
|
346
|
+
// Manually set token (useful after OAuth redirect)
|
|
347
|
+
setToken: async (token) => {
|
|
348
|
+
const result = await sendRequest({ type: "auth", method: "setToken", token });
|
|
349
|
+
if (result.success) {
|
|
350
|
+
sessionToken = token;
|
|
351
|
+
await reconnect();
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
},
|
|
355
|
+
// Check if user is authenticated
|
|
356
|
+
get isAuthenticated() {
|
|
357
|
+
return sessionToken !== null;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Build client object based on config
|
|
363
|
+
const d1Bindings = __D1_BINDINGS__;
|
|
364
|
+
const r2Bindings = __R2_BINDINGS__;
|
|
365
|
+
const kvBindings = __KV_BINDINGS__;
|
|
366
|
+
const authEnabled = __AUTH_ENABLED__;
|
|
367
|
+
|
|
368
|
+
const d1 = {};
|
|
369
|
+
for (const binding of d1Bindings) {
|
|
370
|
+
d1[binding] = createD1Proxy(binding);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const r2 = {};
|
|
374
|
+
for (const binding of r2Bindings) {
|
|
375
|
+
r2[binding] = createR2Proxy(binding);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const kv = {};
|
|
379
|
+
for (const binding of kvBindings) {
|
|
380
|
+
kv[binding] = createKVProxy(binding);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const auth = authEnabled ? createAuthProxy() : null;
|
|
384
|
+
|
|
385
|
+
export default { d1, r2, kv, auth };
|
|
193
386
|
`;
|
|
194
387
|
|
|
195
|
-
|
|
196
|
-
export const
|
|
388
|
+
// Generate core code with config
|
|
389
|
+
export const generateCoreCode = (config = {}) => {
|
|
390
|
+
const { d1 = [], r2 = [], kv = [], auth = false, shared = false } = config;
|
|
391
|
+
return CORE_TEMPLATE
|
|
392
|
+
.replace("__WS_SUFFIX__", shared ? "./?shared" : "./")
|
|
393
|
+
.replace("__D1_BINDINGS__", JSON.stringify(d1))
|
|
394
|
+
.replace("__R2_BINDINGS__", JSON.stringify(r2))
|
|
395
|
+
.replace("__KV_BINDINGS__", JSON.stringify(kv))
|
|
396
|
+
.replace("__AUTH_ENABLED__", String(!!auth));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Legacy exports for backward compatibility
|
|
400
|
+
export const CORE_CODE = CORE_TEMPLATE
|
|
401
|
+
.replace("__WS_SUFFIX__", "./")
|
|
402
|
+
.replace("__D1_BINDINGS__", "[]")
|
|
403
|
+
.replace("__R2_BINDINGS__", "[]")
|
|
404
|
+
.replace("__KV_BINDINGS__", "[]")
|
|
405
|
+
.replace("__AUTH_ENABLED__", "false");
|
|
406
|
+
|
|
407
|
+
export const SHARED_CORE_CODE = CORE_TEMPLATE
|
|
408
|
+
.replace("__WS_SUFFIX__", "./?shared")
|
|
409
|
+
.replace("__D1_BINDINGS__", "[]")
|
|
410
|
+
.replace("__R2_BINDINGS__", "[]")
|
|
411
|
+
.replace("__KV_BINDINGS__", "[]")
|
|
412
|
+
.replace("__AUTH_ENABLED__", "false");
|
package/entry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import moduleMap from "__USER_MODULE__";
|
|
2
2
|
import generatedTypes, { minifiedCore, minifiedSharedCore, coreId } from "__GENERATED_TYPES__";
|
|
3
|
+
import * as exportConfig from "__EXPORT_CONFIG__";
|
|
3
4
|
import { createHandler } from "./handler.js";
|
|
4
5
|
export { SharedExportDO } from "./shared-do.js";
|
|
5
6
|
|
|
6
|
-
export default createHandler(moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore);
|
|
7
|
+
export default createHandler(moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore, exportConfig);
|
package/handler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stringify, parse } from "devalue";
|
|
2
|
-
import { CORE_CODE, SHARED_CORE_CODE } from "./client.js";
|
|
2
|
+
import { generateCoreCode, CORE_CODE, SHARED_CORE_CODE } from "./client.js";
|
|
3
3
|
import { createRpcDispatcher } from "./rpc.js";
|
|
4
|
+
import { handleAuthRoute, getSessionFromRequest, verifySession } from "./auth.js";
|
|
4
5
|
|
|
5
6
|
const JS = "application/javascript; charset=utf-8";
|
|
6
7
|
const TS = "application/typescript; charset=utf-8";
|
|
@@ -13,16 +14,27 @@ const jsResponse = (body, extra = {}) =>
|
|
|
13
14
|
const tsResponse = (body, status = 200) =>
|
|
14
15
|
new Response(body, { status, headers: { "Content-Type": TS, ...CORS, "Cache-Control": "no-cache" } });
|
|
15
16
|
|
|
16
|
-
export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore) => {
|
|
17
|
+
export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, minifiedSharedCore, exportConfig = {}) => {
|
|
17
18
|
// moduleMap: { routePath: moduleNamespace, ... }
|
|
18
19
|
const moduleRoutes = Object.keys(moduleMap); // e.g. ["", "greet", "utils/math"]
|
|
19
20
|
const moduleExportKeys = {};
|
|
20
21
|
for (const [route, mod] of Object.entries(moduleMap)) {
|
|
21
|
-
|
|
22
|
+
const keys = Object.keys(mod);
|
|
23
|
+
if (keys.includes("default")) {
|
|
24
|
+
const modulePath = route || "(root)";
|
|
25
|
+
console.warn(`[export-runtime] WARN: default export in "${modulePath}" is ignored. Use named exports instead.`);
|
|
26
|
+
}
|
|
27
|
+
moduleExportKeys[route] = keys.filter(k => k !== "default");
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
const
|
|
30
|
+
// Export configuration
|
|
31
|
+
const { d1Bindings = [], r2Bindings = [], kvBindings = [], authConfig = null } = exportConfig;
|
|
32
|
+
const hasClient = d1Bindings.length > 0 || r2Bindings.length > 0 || kvBindings.length > 0 || authConfig;
|
|
33
|
+
|
|
34
|
+
// Generate core code with config
|
|
35
|
+
const coreConfig = { d1: d1Bindings, r2: r2Bindings, kv: kvBindings, auth: !!authConfig };
|
|
36
|
+
const coreModuleCode = minifiedCore || generateCoreCode(coreConfig);
|
|
37
|
+
const sharedCoreModuleCode = minifiedSharedCore || generateCoreCode({ ...coreConfig, shared: true });
|
|
26
38
|
const corePath = `/${coreId || crypto.randomUUID()}.js`;
|
|
27
39
|
const sharedCorePath = corePath.replace(".js", "-shared.js");
|
|
28
40
|
|
|
@@ -58,7 +70,9 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
58
70
|
const namedExports = keys
|
|
59
71
|
.map((key) => `export const ${key} = createProxy([${JSON.stringify(route)}, ${JSON.stringify(key)}]);`)
|
|
60
72
|
.join("\n");
|
|
61
|
-
|
|
73
|
+
// Include default export (client) if configured
|
|
74
|
+
const defaultExport = hasClient ? `\nexport { default } from ".${cpath}";` : "";
|
|
75
|
+
return `import { createProxy } from ".${cpath}";\n${namedExports}${defaultExport}`;
|
|
62
76
|
};
|
|
63
77
|
|
|
64
78
|
const buildExportModule = (cpath, route, name) =>
|
|
@@ -66,7 +80,197 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
66
80
|
`const _export = createProxy([${JSON.stringify(route)}, ${JSON.stringify(name)}]);\n` +
|
|
67
81
|
`export default _export;\nexport { _export as ${name} };`;
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
// D1 request handler
|
|
84
|
+
const handleD1Request = async (env, msg) => {
|
|
85
|
+
const { binding, method, sql, params = [], colName } = msg;
|
|
86
|
+
const db = env[binding];
|
|
87
|
+
if (!db) throw new Error(`D1 binding not found: ${binding}`);
|
|
88
|
+
|
|
89
|
+
const stmt = db.prepare(sql).bind(...params);
|
|
90
|
+
switch (method) {
|
|
91
|
+
case "all": return { type: "result", value: await stmt.all() };
|
|
92
|
+
case "first": return { type: "result", value: await stmt.first(colName) };
|
|
93
|
+
case "run": return { type: "result", value: await stmt.run() };
|
|
94
|
+
case "raw": return { type: "result", value: await stmt.raw() };
|
|
95
|
+
default: throw new Error(`Unknown D1 method: ${method}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// R2 request handler
|
|
100
|
+
const handleR2Request = async (env, msg) => {
|
|
101
|
+
const { binding, method, key, value, options } = msg;
|
|
102
|
+
const bucket = env[binding];
|
|
103
|
+
if (!bucket) throw new Error(`R2 binding not found: ${binding}`);
|
|
104
|
+
|
|
105
|
+
switch (method) {
|
|
106
|
+
case "get": {
|
|
107
|
+
const obj = await bucket.get(key, options);
|
|
108
|
+
if (!obj) return { type: "result", value: null };
|
|
109
|
+
// Return object metadata and body as ArrayBuffer
|
|
110
|
+
const body = await obj.arrayBuffer();
|
|
111
|
+
return {
|
|
112
|
+
type: "result",
|
|
113
|
+
value: {
|
|
114
|
+
body: new Uint8Array(body),
|
|
115
|
+
key: obj.key,
|
|
116
|
+
version: obj.version,
|
|
117
|
+
size: obj.size,
|
|
118
|
+
etag: obj.etag,
|
|
119
|
+
httpEtag: obj.httpEtag,
|
|
120
|
+
httpMetadata: obj.httpMetadata,
|
|
121
|
+
customMetadata: obj.customMetadata,
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
case "put": {
|
|
126
|
+
const result = await bucket.put(key, value, options);
|
|
127
|
+
return { type: "result", value: result };
|
|
128
|
+
}
|
|
129
|
+
case "delete": {
|
|
130
|
+
await bucket.delete(key);
|
|
131
|
+
return { type: "result", value: true };
|
|
132
|
+
}
|
|
133
|
+
case "list": {
|
|
134
|
+
const result = await bucket.list(options);
|
|
135
|
+
return { type: "result", value: result };
|
|
136
|
+
}
|
|
137
|
+
case "head": {
|
|
138
|
+
const obj = await bucket.head(key);
|
|
139
|
+
return { type: "result", value: obj };
|
|
140
|
+
}
|
|
141
|
+
default: throw new Error(`Unknown R2 method: ${method}`);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// KV request handler
|
|
146
|
+
const handleKVRequest = async (env, msg) => {
|
|
147
|
+
const { binding, method, key, value, options } = msg;
|
|
148
|
+
const kv = env[binding];
|
|
149
|
+
if (!kv) throw new Error(`KV binding not found: ${binding}`);
|
|
150
|
+
|
|
151
|
+
switch (method) {
|
|
152
|
+
case "get": return { type: "result", value: await kv.get(key, options) };
|
|
153
|
+
case "put": {
|
|
154
|
+
await kv.put(key, value, options);
|
|
155
|
+
return { type: "result", value: true };
|
|
156
|
+
}
|
|
157
|
+
case "delete": {
|
|
158
|
+
await kv.delete(key);
|
|
159
|
+
return { type: "result", value: true };
|
|
160
|
+
}
|
|
161
|
+
case "list": return { type: "result", value: await kv.list(options) };
|
|
162
|
+
case "getWithMetadata": return { type: "result", value: await kv.getWithMetadata(key, options) };
|
|
163
|
+
default: throw new Error(`Unknown KV method: ${method}`);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Auth request handler (WebSocket-based auth operations)
|
|
168
|
+
const handleAuthRequest = async (env, msg, wsSession) => {
|
|
169
|
+
const { method, provider, email, password, name, options, token } = msg;
|
|
170
|
+
|
|
171
|
+
// Handle methods that work without auth config
|
|
172
|
+
if (!authConfig) {
|
|
173
|
+
switch (method) {
|
|
174
|
+
case "signOut":
|
|
175
|
+
return { type: "result", value: { success: true } };
|
|
176
|
+
case "getSession":
|
|
177
|
+
case "getUser":
|
|
178
|
+
return { type: "result", value: null };
|
|
179
|
+
case "signIn.social": {
|
|
180
|
+
const hint = provider ? ` For ${provider} OAuth, also set ${provider.toUpperCase()}_CLIENT_ID/SECRET env vars.` : "";
|
|
181
|
+
return { type: "result", value: { error: `Auth not configured. Add 'auth: true' to cloudflare config in package.json.${hint}` } };
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
if (!["signIn.email", "signUp.email", "setToken"].includes(method)) {
|
|
185
|
+
throw new Error(`Unknown auth method: ${method}`);
|
|
186
|
+
}
|
|
187
|
+
return { type: "result", value: { error: "Auth not configured. Add 'auth: true' to cloudflare config in package.json." } };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const baseUrl = env.WORKER_URL || "https://localhost:8787";
|
|
192
|
+
|
|
193
|
+
switch (method) {
|
|
194
|
+
case "signIn.social": {
|
|
195
|
+
// Return the OAuth URL for client to redirect to
|
|
196
|
+
const callbackUrl = options?.callbackUrl || "/";
|
|
197
|
+
const authUrl = `${baseUrl}/api/auth/signin/${provider}?callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
|
198
|
+
return { type: "result", value: { redirectUrl: authUrl } };
|
|
199
|
+
}
|
|
200
|
+
case "signIn.email": {
|
|
201
|
+
// Forward to better-auth via internal fetch
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch(`${baseUrl}/api/auth/signin/email`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: { "Content-Type": "application/json" },
|
|
206
|
+
body: JSON.stringify({ email, password }),
|
|
207
|
+
});
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
if (response.ok && data.token) {
|
|
210
|
+
return { type: "result", value: { success: true, token: data.token, user: data.user } };
|
|
211
|
+
}
|
|
212
|
+
return { type: "result", value: { error: data.error || "Sign in failed" } };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return { type: "result", value: { error: String(err) } };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
case "signUp.email": {
|
|
218
|
+
try {
|
|
219
|
+
const response = await fetch(`${baseUrl}/api/auth/signup/email`, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
body: JSON.stringify({ email, password, name }),
|
|
223
|
+
});
|
|
224
|
+
const data = await response.json();
|
|
225
|
+
if (response.ok && data.token) {
|
|
226
|
+
return { type: "result", value: { success: true, token: data.token, user: data.user } };
|
|
227
|
+
}
|
|
228
|
+
return { type: "result", value: { error: data.error || "Sign up failed" } };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return { type: "result", value: { error: String(err) } };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
case "signOut": {
|
|
234
|
+
// Clear session via better-auth
|
|
235
|
+
if (wsSession?.token) {
|
|
236
|
+
try {
|
|
237
|
+
await fetch(`${baseUrl}/api/auth/signout`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: {
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
"Cookie": `better-auth.session_token=${wsSession.token}`,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
return { type: "result", value: { success: true } };
|
|
247
|
+
}
|
|
248
|
+
case "getSession": {
|
|
249
|
+
if (!wsSession?.token) return { type: "result", value: null };
|
|
250
|
+
const session = await verifySession(wsSession.token, env, authConfig);
|
|
251
|
+
return { type: "result", value: session };
|
|
252
|
+
}
|
|
253
|
+
case "getUser": {
|
|
254
|
+
if (!wsSession?.token) return { type: "result", value: null };
|
|
255
|
+
const session = await verifySession(wsSession.token, env, authConfig);
|
|
256
|
+
return { type: "result", value: session?.user || null };
|
|
257
|
+
}
|
|
258
|
+
case "setToken": {
|
|
259
|
+
// Client sends token after OAuth redirect
|
|
260
|
+
if (token) {
|
|
261
|
+
const session = await verifySession(token, env, authConfig);
|
|
262
|
+
if (session) {
|
|
263
|
+
return { type: "result", value: { success: true, session } };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { type: "result", value: { error: "Invalid token" } };
|
|
267
|
+
}
|
|
268
|
+
default:
|
|
269
|
+
throw new Error(`Unknown auth method: ${method}`);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const dispatchMessage = async (dispatcher, msg, env, wsSession) => {
|
|
70
274
|
const { type, path = [], args = [], instanceId, iteratorId, streamId } = msg;
|
|
71
275
|
switch (type) {
|
|
72
276
|
case "ping": return { type: "pong" };
|
|
@@ -82,16 +286,39 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
82
286
|
case "iterate-return": return dispatcher.rpcIterateReturn(iteratorId);
|
|
83
287
|
case "stream-read": return dispatcher.rpcStreamRead(streamId);
|
|
84
288
|
case "stream-cancel": return dispatcher.rpcStreamCancel(streamId);
|
|
289
|
+
// Client requests
|
|
290
|
+
case "d1": return handleD1Request(env, msg);
|
|
291
|
+
case "r2": return handleR2Request(env, msg);
|
|
292
|
+
case "kv": return handleKVRequest(env, msg);
|
|
293
|
+
case "auth": return handleAuthRequest(env, msg, wsSession);
|
|
85
294
|
}
|
|
86
295
|
};
|
|
87
296
|
|
|
88
|
-
const wireWebSocket = (server, dispatcher, onClose) => {
|
|
297
|
+
const wireWebSocket = (server, dispatcher, env, onClose) => {
|
|
298
|
+
// Track session state for this WebSocket connection
|
|
299
|
+
const wsSession = { token: null };
|
|
300
|
+
|
|
89
301
|
server.addEventListener("message", async (event) => {
|
|
90
302
|
let id;
|
|
91
303
|
try {
|
|
92
304
|
const msg = parse(event.data);
|
|
93
305
|
id = msg.id;
|
|
94
|
-
|
|
306
|
+
|
|
307
|
+
// Handle auth token updates (on reconnect or explicit setToken)
|
|
308
|
+
if (msg.type === "auth" && msg.token && !msg.method) {
|
|
309
|
+
// Direct token send on reconnect - just update session
|
|
310
|
+
wsSession.token = msg.token;
|
|
311
|
+
server.send(stringify({ type: "auth-result", id, success: true }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const result = await dispatchMessage(dispatcher, msg, env, wsSession);
|
|
316
|
+
|
|
317
|
+
// Extract token from auth responses
|
|
318
|
+
if (result?.value?.token && msg.type === "auth") {
|
|
319
|
+
wsSession.token = result.value.token;
|
|
320
|
+
}
|
|
321
|
+
|
|
95
322
|
if (result) server.send(stringify({ ...result, id }));
|
|
96
323
|
} catch (err) {
|
|
97
324
|
if (id !== undefined) server.send(stringify({ type: "error", id, error: String(err) }));
|
|
@@ -114,10 +341,10 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
114
341
|
if (isShared && env?.SHARED_EXPORT) {
|
|
115
342
|
const room = url.searchParams.get("room") || "default";
|
|
116
343
|
const stub = env.SHARED_EXPORT.get(env.SHARED_EXPORT.idFromName(room));
|
|
117
|
-
wireWebSocket(server, stub);
|
|
344
|
+
wireWebSocket(server, stub, env);
|
|
118
345
|
} else {
|
|
119
346
|
const dispatcher = createRpcDispatcher(moduleMap);
|
|
120
|
-
wireWebSocket(server, dispatcher, () => dispatcher.clearAll());
|
|
347
|
+
wireWebSocket(server, dispatcher, env, () => dispatcher.clearAll());
|
|
121
348
|
}
|
|
122
349
|
|
|
123
350
|
return new Response(null, { status: 101, webSocket: client });
|
|
@@ -126,6 +353,11 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
126
353
|
// --- HTTP routing ---
|
|
127
354
|
const pathname = url.pathname;
|
|
128
355
|
|
|
356
|
+
// Auth routes (handled by better-auth)
|
|
357
|
+
if (authConfig && pathname.startsWith("/api/auth/")) {
|
|
358
|
+
return handleAuthRoute(request, env, authConfig);
|
|
359
|
+
}
|
|
360
|
+
|
|
129
361
|
// Core modules
|
|
130
362
|
if (pathname === corePath) return jsResponse(coreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
131
363
|
if (pathname === sharedCorePath) return jsResponse(sharedCoreModuleCode, { "Cache-Control": IMMUTABLE });
|
|
@@ -154,7 +386,13 @@ export const createHandler = (moduleMap, generatedTypes, minifiedCore, coreId, m
|
|
|
154
386
|
const cpath = isShared ? sharedCorePath : corePath;
|
|
155
387
|
|
|
156
388
|
const resolved = resolveRoute(pathname);
|
|
157
|
-
if (!resolved)
|
|
389
|
+
if (!resolved) {
|
|
390
|
+
// Fallback to static assets if ASSETS binding is available
|
|
391
|
+
if (env?.ASSETS) {
|
|
392
|
+
return env.ASSETS.fetch(request);
|
|
393
|
+
}
|
|
394
|
+
return new Response("Not found", { status: 404 });
|
|
395
|
+
}
|
|
158
396
|
|
|
159
397
|
const { route, exportName } = resolved;
|
|
160
398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "export-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Cloudflare Workers ESM Export Framework Runtime",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -31,11 +31,22 @@
|
|
|
31
31
|
"client.js",
|
|
32
32
|
"rpc.js",
|
|
33
33
|
"shared-do.js",
|
|
34
|
-
"
|
|
34
|
+
"auth.js",
|
|
35
|
+
"bin/generate-types.mjs",
|
|
36
|
+
"bin/auth.mjs"
|
|
35
37
|
],
|
|
36
38
|
"dependencies": {
|
|
39
|
+
"better-auth": "^1.2.0",
|
|
37
40
|
"devalue": "^5.1.1",
|
|
38
41
|
"oxc-minify": "^0.121.0",
|
|
39
42
|
"oxc-parser": "^0.121.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"better-auth": "^1.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"better-auth": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
40
51
|
}
|
|
41
52
|
}
|
package/rpc.js
CHANGED
|
@@ -65,6 +65,8 @@ export function createRpcDispatcher(moduleMap) {
|
|
|
65
65
|
// path = [route, ...exportPath] — route selects the module, exportPath walks its exports
|
|
66
66
|
const splitPath = (path) => {
|
|
67
67
|
const [route, ...rest] = path;
|
|
68
|
+
// Reject default export access
|
|
69
|
+
if (rest[0] === "default") throw new Error("Export not found: default");
|
|
68
70
|
return { exports: resolveModule(route), exportPath: rest };
|
|
69
71
|
};
|
|
70
72
|
|