elm-ssr 1.0.4 → 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
@@ -536,6 +536,10 @@ const getConfig = (env: any): Auth0Config => ({
536
536
  callbackUrl: (env?.AUTH0_CALLBACK_URL as string) ?? "http://localhost:8787/api/auth/callback",
537
537
  });
538
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
+
539
543
  export const handleAuth = async (
540
544
  request: Request,
541
545
  env: any,
@@ -557,14 +561,14 @@ export const handleAuth = async (
557
561
  redirect_uri: config.callbackUrl,
558
562
  scope: "openid profile email",
559
563
  });
560
- return Response.redirect(\`https://\${config.domain}/authorize?\${params}\`, 302);
564
+ return Response.redirect(\`\${proto(config.domain)}://\${config.domain}/authorize?\${params}\`, 302);
561
565
  }
562
566
 
563
567
  if (url.pathname === "/api/auth/callback") {
564
568
  const code = url.searchParams.get("code");
565
569
  if (!code) return new Response("Missing code", { status: 400 });
566
570
 
567
- const tokenRes = await fetch(\`https://\${config.domain}/oauth/token\`, {
571
+ const tokenRes = await fetch(\`\${proto(config.domain)}://\${config.domain}/oauth/token\`, {
568
572
  method: "POST",
569
573
  headers: { "content-type": "application/json" },
570
574
  body: JSON.stringify({
@@ -618,7 +622,7 @@ export const handleAuth = async (
618
622
  status: 302,
619
623
  headers: {
620
624
  "Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly",
621
- Location: \`https://\${config.domain}/oidc/logout?\${params}\`,
625
+ Location: \`\${proto(config.domain)}://\${config.domain}/oidc/logout?\${params}\`,
622
626
  },
623
627
  });
624
628
  }
@@ -652,9 +656,10 @@ const runtimeTemplate = (appRoot, db = false, auth = undefined) => {
652
656
  imports.push(`import { sessionEffects } from "elm-ssr/sessions";`);
653
657
  imports.push(`import type { Middleware } from "elm-ssr/http";`);
654
658
  imports.push(`import type { RequestSession } from "elm-ssr/sessions";`);
655
- imports.push(`import { createAuth, handleAuth } from "./src/Endpoints/Auth";`);
659
+ imports.push(`import { createAuth } from "./src/Endpoints/Auth";`);
656
660
  } else if (isAuth0) {
657
661
  imports.push(`import { memorySessionStore } from "elm-ssr/sessions";`);
662
+ imports.push(`import type { Middleware } from "elm-ssr/http";`);
658
663
  imports.push(`import { handleAuth } from "./src/Endpoints/Auth";`);
659
664
  }
660
665
 
@@ -712,7 +717,9 @@ if (typeof Bun !== "undefined") {
712
717
  }`;
713
718
 
714
719
  const effectsConfig = isBetterAuth
715
- ? `,\n effects: sessionEffects(${baseEffectsBody}),\n middlewares: [betterAuthBridge]`
720
+ ? `,\n effects: sessionEffects(${baseEffectsBody}),\n middlewares: [betterAuthHandler, betterAuthBridge]`
721
+ : isAuth0
722
+ ? `,\n effects: ${baseEffectsBody},\n middlewares: [auth0Handler]`
716
723
  : `,\n effects: ${baseEffectsBody}`;
717
724
 
718
725
  // Auth0 keeps elm-ssr session middleware; BetterAuth manages sessions itself.
@@ -732,10 +739,30 @@ if (typeof (globalThis as any).Bun !== "undefined") {
732
739
  const getAuthEnv = (env: any): any =>
733
740
  bunAuthDb && !env?.DB ? { ...(env ?? {}), DB: bunAuthDb } : env;
734
741
 
735
- // Bridge: reads BetterAuth session on every request so Loader.requireUser works.
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
+ });
758
+ }
759
+ return getAuth(context.env).handler(context.request);
760
+ };
761
+
762
+ // Bridge: reads BetterAuth session before every Elm render so Loader.requireUser works.
736
763
  const betterAuthBridge: Middleware = async (context, next) => {
737
764
  try {
738
- const session = await createAuth(getAuthEnv(context.env)).api.getSession({
765
+ const session = await getAuth(context.env).api.getSession({
739
766
  headers: context.request.headers,
740
767
  });
741
768
  const user = session?.user
@@ -758,43 +785,31 @@ const betterAuthBridge: Middleware = async (context, next) => {
758
785
  };
759
786
  `;
760
787
  } else if (isAuth0) {
761
- authInit = `\nexport const sessionStore = memorySessionStore();\n`;
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 });
800
+ };
801
+ `;
762
802
  sessionsConfig = `,
763
803
  sessions: {
764
804
  secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
765
805
  store: sessionStore,
766
806
  secure: false
767
807
  },
768
- csrf: true`;
808
+ csrf: { skipPaths: ["/api/auth/"] }`;
769
809
  }
770
810
 
771
- let authIntercept = '';
772
- if (isBetterAuth) {
773
- authIntercept = `
774
- // Delegate all /api/auth/* requests to BetterAuth, injecting the local db when needed.
775
- const baseWorkerFetch = worker.fetch;
776
- worker.fetch = async (request, env, ctx) => {
777
- const url = new URL(request.url);
778
- if (url.pathname.startsWith("/api/auth/")) {
779
- return handleAuth(request, getAuthEnv(env));
780
- }
781
- return baseWorkerFetch(request, env, ctx);
782
- };
783
- `;
784
- } else if (isAuth0) {
785
- authIntercept = `
786
- // Delegate all /api/auth/* requests to the Auth0 handler.
787
- const baseWorkerFetch = worker.fetch;
788
- worker.fetch = async (request, env, ctx) => {
789
- const url = new URL(request.url);
790
- if (url.pathname.startsWith("/api/auth/")) {
791
- const secret = (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars";
792
- return handleAuth(request, env, { store: sessionStore, secret });
793
- }
794
- return baseWorkerFetch(request, env, ctx);
795
- };
796
- `;
797
- }
811
+ // Auth routing is handled entirely through elm-ssr's middlewares option —
812
+ // no worker.fetch wrapping needed.
798
813
 
799
814
  return `${imports.join("\n")}
800
815
 
@@ -888,7 +903,6 @@ export const worker = createWorkerApp({
888
903
  routes,
889
904
  createFlags${sessionsConfig}${effectsConfig}
890
905
  });
891
- ${authIntercept}
892
906
  `;
893
907
  };
894
908
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-ssr",
3
- "version": "1.0.4",
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>",