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 +12 -1
- package/lib/scaffold.mjs +99 -80
- package/package.json +1 -1
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
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
const createAuth = (env: any) =>
|
|
505
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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 {
|
|
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: [
|
|
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 (
|
|
756
|
-
authInit =
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
784
|
+
return next(context);
|
|
777
785
|
};
|
|
778
786
|
`;
|
|
779
787
|
} else if (isAuth0) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
+
"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>",
|