elm-ssr 1.0.3 → 1.0.5

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/bin/elm-ssr.mjs CHANGED
@@ -13,6 +13,11 @@ const defaultRootPath = process.cwd();
13
13
  const args = process.argv.slice(2);
14
14
  const command = args[0] ?? "help";
15
15
 
16
+ const ownPkg = JSON.parse(
17
+ await readFile(new URL("../package.json", import.meta.url), "utf8")
18
+ );
19
+ const VERSION = ownPkg.version;
20
+
16
21
  const findFlagValue = (flagName) => {
17
22
  const index = args.indexOf(flagName);
18
23
  return index >= 0 ? args[index + 1] : undefined;
@@ -71,7 +76,7 @@ const run = async (cmd, cmdArgs, cwd = rootPath) => {
71
76
  };
72
77
 
73
78
  const printHelp = () => {
74
- console.log(`elm-ssr commands
79
+ console.log(`elm-ssr ${VERSION}
75
80
 
76
81
  build Generate wrapper modules and compile configured Elm SSR apps
77
82
  compress Pre-compress island and app bundles using Gzip for faster edge delivery
@@ -85,6 +90,7 @@ const printHelp = () => {
85
90
  query Generate type-safe Elm Db modules from SQL table definitions
86
91
  info Print current workspace package and configured app names
87
92
  migrate ... Apply / revert / inspect SQL migrations (see: elm-ssr migrate --help)
93
+ version Print the elm-ssr version
88
94
  `);
89
95
  };
90
96
 
@@ -105,6 +111,11 @@ const requireConfig = () => {
105
111
  }
106
112
  };
107
113
 
114
+ if (command === "--version" || command === "-v" || command === "version") {
115
+ console.log(VERSION);
116
+ process.exit(0);
117
+ }
118
+
108
119
  switch (command) {
109
120
  case "build":
110
121
  case "compress":
package/lib/scaffold.mjs CHANGED
@@ -495,23 +495,17 @@ view user =
495
495
  `;
496
496
 
497
497
  const betterAuthEndpointTemplate = () => `import { betterAuth } from "better-auth";
498
- import type { Middleware } from "elm-ssr/http";
499
- import type { RequestSession } from "elm-ssr/sessions";
500
-
501
- // BetterAuth is initialised per-request to pick up the right DB binding.
502
- // Production (Cloudflare Workers): env.DB is the D1 binding.
503
- // Local dev (Bun): falls back to bun:sqlite using app.db.
504
- const createAuth = (env: any) => {
505
- const db = env?.DB ?? (() => {
506
- // eslint-disable-next-line @typescript-eslint/no-require-imports
507
- const { Database } = require("bun:sqlite");
508
- return new Database("app.db");
509
- })();
510
-
511
- return betterAuth({
498
+
499
+ // BetterAuth is initialised per-request to pick up the right database binding.
500
+ // On Cloudflare Workers env.DB is the D1 binding; on Bun (local dev) the
501
+ // runtime injects a bun:sqlite Database as env.DB see runtime.ts.
502
+ // Pass env.DB directly so BetterAuth's adapter auto-detects the dialect
503
+ // (D1 via "batch"/"exec"/"prepare", bun:sqlite via "fileControl").
504
+ export const createAuth = (env: any) =>
505
+ betterAuth({
512
506
  baseURL: (env?.BETTER_AUTH_URL as string) ?? "http://localhost:8787",
513
507
  secret: (env?.BETTER_AUTH_SECRET as string) ?? "change-me-in-production",
514
- database: { type: "sqlite", db },
508
+ database: env?.DB,
515
509
  emailAndPassword: { enabled: true },
516
510
  // Uncomment to add social providers (add matching env vars in .dev.vars):
517
511
  // socialProviders: {
@@ -521,39 +515,9 @@ const createAuth = (env: any) => {
521
515
  // },
522
516
  // },
523
517
  });
524
- };
525
518
 
526
519
  export const handleAuth = (request: Request, env: any): Promise<Response> =>
527
520
  createAuth(env).handler(request);
528
-
529
- // Reads the BetterAuth session from the request and returns the user.
530
- const getSessionUser = async (
531
- request: Request,
532
- env: any
533
- ): Promise<{ email: string; name: string } | null> => {
534
- try {
535
- const session = await createAuth(env).api.getSession({ headers: request.headers });
536
- if (!session?.user) return null;
537
- return { email: session.user.email, name: session.user.name ?? session.user.email };
538
- } catch {
539
- return null;
540
- }
541
- };
542
-
543
- // elm-ssr middleware: reads BetterAuth's session and injects the user into
544
- // context.session so Loader.session / Loader.requireUser work on every request.
545
- export const betterAuthMiddleware: Middleware = async (context, next) => {
546
- const user = await getSessionUser(context.request, context.env);
547
- context.session = {
548
- id: user ? \`ba:\${user.email}\` : "",
549
- data: user,
550
- csrf: "",
551
- dirty: false,
552
- destroyed: false,
553
- isNew: false,
554
- } as RequestSession;
555
- return next(context);
556
- };
557
521
  `;
558
522
 
559
523
  const auth0EndpointTemplate = () => `import { generateSessionId, signValue, generateCsrfToken, readSignedCookie } from "elm-ssr/sessions";
@@ -572,6 +536,10 @@ const getConfig = (env: any): Auth0Config => ({
572
536
  callbackUrl: (env?.AUTH0_CALLBACK_URL as string) ?? "http://localhost:8787/api/auth/callback",
573
537
  });
574
538
 
539
+ // Use http for localhost (dev/test mock servers), https for real Auth0 domains.
540
+ const proto = (domain: string) =>
541
+ domain.startsWith("localhost") || domain.startsWith("127.") ? "http" : "https";
542
+
575
543
  export const handleAuth = async (
576
544
  request: Request,
577
545
  env: any,
@@ -593,14 +561,14 @@ export const handleAuth = async (
593
561
  redirect_uri: config.callbackUrl,
594
562
  scope: "openid profile email",
595
563
  });
596
- return Response.redirect(\`https://\${config.domain}/authorize?\${params}\`, 302);
564
+ return Response.redirect(\`\${proto(config.domain)}://\${config.domain}/authorize?\${params}\`, 302);
597
565
  }
598
566
 
599
567
  if (url.pathname === "/api/auth/callback") {
600
568
  const code = url.searchParams.get("code");
601
569
  if (!code) return new Response("Missing code", { status: 400 });
602
570
 
603
- const tokenRes = await fetch(\`https://\${config.domain}/oauth/token\`, {
571
+ const tokenRes = await fetch(\`\${proto(config.domain)}://\${config.domain}/oauth/token\`, {
604
572
  method: "POST",
605
573
  headers: { "content-type": "application/json" },
606
574
  body: JSON.stringify({
@@ -654,7 +622,7 @@ export const handleAuth = async (
654
622
  status: 302,
655
623
  headers: {
656
624
  "Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly",
657
- Location: \`https://\${config.domain}/oidc/logout?\${params}\`,
625
+ Location: \`\${proto(config.domain)}://\${config.domain}/oidc/logout?\${params}\`,
658
626
  },
659
627
  });
660
628
  }
@@ -686,9 +654,12 @@ const runtimeTemplate = (appRoot, db = false, auth = undefined) => {
686
654
 
687
655
  if (isBetterAuth) {
688
656
  imports.push(`import { sessionEffects } from "elm-ssr/sessions";`);
689
- imports.push(`import { handleAuth, betterAuthMiddleware } from "./src/Endpoints/Auth";`);
657
+ imports.push(`import type { Middleware } from "elm-ssr/http";`);
658
+ imports.push(`import type { RequestSession } from "elm-ssr/sessions";`);
659
+ imports.push(`import { createAuth } from "./src/Endpoints/Auth";`);
690
660
  } else if (isAuth0) {
691
661
  imports.push(`import { memorySessionStore } from "elm-ssr/sessions";`);
662
+ imports.push(`import type { Middleware } from "elm-ssr/http";`);
692
663
  imports.push(`import { handleAuth } from "./src/Endpoints/Auth";`);
693
664
  }
694
665
 
@@ -746,51 +717,100 @@ if (typeof Bun !== "undefined") {
746
717
  }`;
747
718
 
748
719
  const effectsConfig = isBetterAuth
749
- ? `,\n effects: sessionEffects(${baseEffectsBody}),\n middlewares: [betterAuthMiddleware]`
720
+ ? `,\n effects: sessionEffects(${baseEffectsBody}),\n middlewares: [betterAuthHandler, betterAuthBridge]`
721
+ : isAuth0
722
+ ? `,\n effects: ${baseEffectsBody},\n middlewares: [auth0Handler]`
750
723
  : `,\n effects: ${baseEffectsBody}`;
751
724
 
752
725
  // Auth0 keeps elm-ssr session middleware; BetterAuth manages sessions itself.
753
726
  let sessionsConfig = '';
754
727
  let authInit = '';
755
- if (isAuth0) {
756
- authInit = `\nexport const sessionStore = memorySessionStore();\n`;
757
- sessionsConfig = `,
758
- sessions: {
759
- secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
760
- store: sessionStore,
761
- secure: false
762
- },
763
- csrf: true`;
728
+ if (isBetterAuth) {
729
+ authInit = `
730
+ // Local dev: open a SQLite database so BetterAuth works without a Cloudflare D1 binding.
731
+ // On Cloudflare Workers this block is eliminated by esbuild (typeof Bun is statically "undefined").
732
+ let bunAuthDb: any = undefined;
733
+ if (typeof (globalThis as any).Bun !== "undefined") {
734
+ const sqliteModule = "bun" + ":sqlite";
735
+ const { Database } = require(sqliteModule);
736
+ bunAuthDb = new Database(import.meta.dir + "/app.db");
737
+ }
738
+
739
+ const getAuthEnv = (env: any): any =>
740
+ bunAuthDb && !env?.DB ? { ...(env ?? {}), DB: bunAuthDb } : env;
741
+
742
+ // Singleton: BetterAuth is initialised once per worker isolate, not per request.
743
+ // Cloudflare's D1 binding is stable within an isolate; bun:sqlite is module-level too.
744
+ let _auth: ReturnType<typeof createAuth> | null = null;
745
+ const getAuth = (env: any) => (_auth ??= createAuth(getAuthEnv(env)));
746
+
747
+ // Handles all /api/auth/* requests: BetterAuth routes + the dashboard validation endpoint.
748
+ const betterAuthHandler: Middleware = async (context, next) => {
749
+ if (!context.url.pathname.startsWith("/api/auth/")) return next(context);
750
+ // BetterAuth's online dashboard calls this to confirm the server is reachable
751
+ // before saving configuration changes. Not in BetterAuth's npm package.
752
+ if (context.url.pathname === "/api/auth/dash/validate") {
753
+ const challenge = context.url.searchParams.get("challenge");
754
+ return new Response(challenge ?? JSON.stringify({ ok: true }), {
755
+ status: 200,
756
+ headers: { "content-type": challenge ? "text/plain" : "application/json" },
757
+ });
764
758
  }
759
+ return getAuth(context.env).handler(context.request);
760
+ };
765
761
 
766
- let authIntercept = '';
767
- if (isBetterAuth) {
768
- authIntercept = `
769
- // Delegate all /api/auth/* requests to BetterAuth.
770
- const baseWorkerFetch = worker.fetch;
771
- worker.fetch = async (request, env, ctx) => {
772
- const url = new URL(request.url);
773
- if (url.pathname.startsWith("/api/auth/")) {
774
- return handleAuth(request, env);
762
+ // Bridge: reads BetterAuth session before every Elm render so Loader.requireUser works.
763
+ const betterAuthBridge: Middleware = async (context, next) => {
764
+ try {
765
+ const session = await getAuth(context.env).api.getSession({
766
+ headers: context.request.headers,
767
+ });
768
+ const user = session?.user
769
+ ? { email: session.user.email, name: session.user.name ?? session.user.email }
770
+ : null;
771
+ context.session = {
772
+ id: user ? \`ba:\${user.email}\` : "",
773
+ data: user,
774
+ csrf: "",
775
+ dirty: false,
776
+ destroyed: false,
777
+ isNew: false,
778
+ } as RequestSession;
779
+ } catch {
780
+ context.session = {
781
+ id: "", data: null, csrf: "", dirty: false, destroyed: false, isNew: false,
782
+ } as RequestSession;
775
783
  }
776
- return baseWorkerFetch(request, env, ctx);
784
+ return next(context);
777
785
  };
778
786
  `;
779
787
  } else if (isAuth0) {
780
- authIntercept = `
781
- // Delegate all /api/auth/* requests to the Auth0 handler.
782
- const baseWorkerFetch = worker.fetch;
783
- worker.fetch = async (request, env, ctx) => {
784
- const url = new URL(request.url);
785
- if (url.pathname.startsWith("/api/auth/")) {
786
- const secret = (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars";
787
- return handleAuth(request, env, { store: sessionStore, secret });
788
- }
789
- return baseWorkerFetch(request, env, ctx);
788
+ authInit = `
789
+ export const sessionStore = memorySessionStore();
790
+
791
+ // Handles all /api/auth/* requests (login redirect, OAuth callback, logout).
792
+ // Runs inside elm-ssr's middleware stack so logging and error handling apply.
793
+ // Sets session.isNew = false to prevent sessionMiddleware from appending a
794
+ // competing Set-Cookie on top of the one the auth handler already set.
795
+ const auth0Handler: Middleware = async (context, next) => {
796
+ if (!context.url.pathname.startsWith("/api/auth/")) return next(context);
797
+ if (context.session) context.session.isNew = false;
798
+ const secret = (context.env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars";
799
+ return handleAuth(context.request, context.env, { store: sessionStore, secret });
790
800
  };
791
801
  `;
802
+ sessionsConfig = `,
803
+ sessions: {
804
+ secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
805
+ store: sessionStore,
806
+ secure: false
807
+ },
808
+ csrf: { skipPaths: ["/api/auth/"] }`;
792
809
  }
793
810
 
811
+ // Auth routing is handled entirely through elm-ssr's middlewares option —
812
+ // no worker.fetch wrapping needed.
813
+
794
814
  return `${imports.join("\n")}
795
815
 
796
816
  const elmModule = ElmRuntime as CompiledElmModule;
@@ -883,7 +903,6 @@ export const worker = createWorkerApp({
883
903
  routes,
884
904
  createFlags${sessionsConfig}${effectsConfig}
885
905
  });
886
- ${authIntercept}
887
906
  `;
888
907
  };
889
908
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-ssr",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Elm-first SSR library and framework for Cloudflare Workers (and Bun): file-based routes/islands, backend-neutral effect adapters (KV/D1/Redis/Postgres), background tasks (waitUntil/Queues), SQL-file migrations, CLI scaffold + build.",
5
5
  "license": "MIT",
6
6
  "author": "Michał Majchrzak <michmajchrzak@gmail.com>",