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 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
+ }
@@ -9,25 +9,69 @@ import { fileURLToPath } from "url";
9
9
 
10
10
  const cwd = process.cwd();
11
11
 
12
- // Read wrangler.toml to find source root
13
- const wranglerPath = path.join(cwd, "wrangler.toml");
14
- if (!fs.existsSync(wranglerPath)) {
15
- console.error("wrangler.toml not found in", cwd);
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
- const wranglerContent = fs.readFileSync(wranglerPath, "utf8");
19
- const aliasMatch = wranglerContent.match(/"__USER_MODULE__"\s*=\s*"([^"]+)"/);
20
- if (!aliasMatch) {
21
- console.error('Could not find __USER_MODULE__ alias in wrangler.toml');
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
- // Derive source root directory from the alias (e.g., "./src/index.ts" → "./src")
26
- const aliasTarget = aliasMatch[1];
27
- // If alias points to the module map, resolve src dir from it; otherwise derive from file
28
- const srcDir = aliasTarget.includes(".export-module-map")
29
- ? path.resolve(cwd, "src")
30
- : path.resolve(cwd, path.dirname(aliasTarget.replace(/^\.\//, "")));
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 { CORE_CODE, SHARED_CORE_CODE } = await import(path.join(__dirname, "..", "client.js"));
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
- const ws = new WebSocket(_u.href);
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 ready = new Promise((resolve, reject) => {
118
- ws.onopen = () => {
119
+ const setupWs = (socket) => {
120
+ socket.onopen = () => {
119
121
  keepaliveInterval = setInterval(() => {
120
- if (ws.readyState === WebSocket.OPEN) ws.send(stringify({ type: "ping", id: 0 }));
122
+ if (socket.readyState === WebSocket.OPEN) socket.send(stringify({ type: "ping", id: 0 }));
121
123
  }, 30000);
122
- resolve();
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
- ws.onerror = reject;
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
- ws.onclose = () => { clearInterval(keepaliveInterval); };
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
- export const CORE_CODE = CORE_TEMPLATE.replace("__WS_SUFFIX__", "./");
196
- export const SHARED_CORE_CODE = CORE_TEMPLATE.replace("__WS_SUFFIX__", "./?shared");
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
- moduleExportKeys[route] = Object.keys(mod);
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
- const coreModuleCode = minifiedCore || CORE_CODE;
25
- const sharedCoreModuleCode = minifiedSharedCore || SHARED_CORE_CODE;
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
- return `import { createProxy } from ".${cpath}";\n${namedExports}`;
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
- const dispatchMessage = async (dispatcher, msg) => {
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
- const result = await dispatchMessage(dispatcher, msg);
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) return new Response("Not found", { status: 404 });
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.9",
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
- "bin/generate-types.mjs"
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