elm-ssr 0.91.3 → 0.91.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 +8 -6
- package/lib/scaffold.mjs +61 -13
- package/package.json +1 -1
- package/src/app.ts +11 -5
- package/src/sessions/index.ts +1 -0
- package/src/sessions/middleware.ts +16 -9
- package/src/sessions/store.ts +2 -1
package/bin/elm-ssr.mjs
CHANGED
|
@@ -77,9 +77,9 @@ const printHelp = () => {
|
|
|
77
77
|
compress Pre-compress island and app bundles using Gzip for faster edge delivery
|
|
78
78
|
dev Build and start wrangler dev using the current workspace config
|
|
79
79
|
init <name> Initialize a self-contained single-app project in the current directory
|
|
80
|
-
(use --db to wire SQLite/migrations, --auth betterAuth|auth0 for auth guards)
|
|
80
|
+
(use --db to wire SQLite/migrations, --auth betterAuth|auth0 for auth guards, --tailwind for Tailwind CSS)
|
|
81
81
|
new <name> Create a new app at <workspace>/<name>/ and register it in elm-ssr.config.json
|
|
82
|
-
(use --in <subdir> to group, --db for SQLite, --auth betterAuth|auth0 for auth)
|
|
82
|
+
(use --in <subdir> to group, --db for SQLite, --auth betterAuth|auth0 for auth, --tailwind for Tailwind)
|
|
83
83
|
routes Print configured apps and their public modules
|
|
84
84
|
route <path> Scaffold a new route (standard HTML, --api JSON, --ws WebSocket, or --sse EventSource)
|
|
85
85
|
query Generate type-safe Elm Db modules from SQL table definitions
|
|
@@ -196,11 +196,12 @@ switch (command) {
|
|
|
196
196
|
const name = args[1];
|
|
197
197
|
|
|
198
198
|
if (!name) {
|
|
199
|
-
console.error("Usage: elm-ssr init <name> [--db] [--auth betterAuth|auth0]");
|
|
199
|
+
console.error("Usage: elm-ssr init <name> [--db] [--auth betterAuth|auth0] [--tailwind]");
|
|
200
200
|
process.exit(1);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
const db = args.includes("--db");
|
|
204
|
+
const tailwind = args.includes("--tailwind");
|
|
204
205
|
let auth = findFlagValue("--auth");
|
|
205
206
|
if (auth) {
|
|
206
207
|
if (auth === "better-auth" || auth === "betterAuth") {
|
|
@@ -211,7 +212,7 @@ switch (command) {
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
const created = await createAppScaffold(rootPath, name, { root: ".", db, auth });
|
|
215
|
+
const created = await createAppScaffold(rootPath, name, { root: ".", db, auth, tailwind });
|
|
215
216
|
console.log(`Initialized ${created.name} in current directory`);
|
|
216
217
|
break;
|
|
217
218
|
}
|
|
@@ -220,7 +221,7 @@ switch (command) {
|
|
|
220
221
|
const name = args[1];
|
|
221
222
|
|
|
222
223
|
if (!name) {
|
|
223
|
-
console.error("Usage: elm-ssr new <name> [--in <subdir>] [--db] [--auth betterAuth|auth0]");
|
|
224
|
+
console.error("Usage: elm-ssr new <name> [--in <subdir>] [--db] [--auth betterAuth|auth0] [--tailwind]");
|
|
224
225
|
console.error(" Default location: <workspace>/<name>/");
|
|
225
226
|
console.error(" Use --in apps to place it under <workspace>/apps/<name>/, etc.");
|
|
226
227
|
process.exit(1);
|
|
@@ -230,6 +231,7 @@ switch (command) {
|
|
|
230
231
|
const appRoot = subdir ? `${subdir.replace(/\/+$/, "")}/${name}` : name;
|
|
231
232
|
|
|
232
233
|
const db = args.includes("--db");
|
|
234
|
+
const tailwind = args.includes("--tailwind");
|
|
233
235
|
let auth = findFlagValue("--auth");
|
|
234
236
|
if (auth) {
|
|
235
237
|
if (auth === "better-auth" || auth === "betterAuth") {
|
|
@@ -240,7 +242,7 @@ switch (command) {
|
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
const created = await createAppScaffold(rootPath, name, { root: appRoot, db, auth });
|
|
245
|
+
const created = await createAppScaffold(rootPath, name, { root: appRoot, db, auth, tailwind });
|
|
244
246
|
console.log(`Created ${created.name} at ${created.root}`);
|
|
245
247
|
break;
|
|
246
248
|
}
|
package/lib/scaffold.mjs
CHANGED
|
@@ -368,28 +368,57 @@ view user =
|
|
|
368
368
|
}
|
|
369
369
|
`;
|
|
370
370
|
|
|
371
|
-
const authEndpointTemplate = (authProvider) => `
|
|
371
|
+
const authEndpointTemplate = (authProvider) => `import { generateSessionId, signValue, generateCsrfToken, readSignedCookie } from "elm-ssr/sessions";
|
|
372
|
+
|
|
373
|
+
export const handleAuth = async (
|
|
374
|
+
request: Request,
|
|
375
|
+
env: any,
|
|
376
|
+
options?: { store?: any; secret?: string }
|
|
377
|
+
): Promise<Response> => {
|
|
372
378
|
const url = new URL(request.url);
|
|
373
379
|
|
|
374
380
|
if (url.pathname === "/api/auth/login") {
|
|
375
381
|
// Mock login session update for ${authProvider}
|
|
382
|
+
let cookieVal = "__elm_ssr_session=" + encodeURIComponent(JSON.stringify({
|
|
383
|
+
user: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" }
|
|
384
|
+
})) + "; Path=/; HttpOnly; SameSite=Lax";
|
|
385
|
+
|
|
386
|
+
if (options?.store && options?.secret) {
|
|
387
|
+
const sessionId = generateSessionId();
|
|
388
|
+
await options.store.set(sessionId, {
|
|
389
|
+
data: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" },
|
|
390
|
+
csrf: generateCsrfToken()
|
|
391
|
+
});
|
|
392
|
+
const signed = await signValue(options.secret, sessionId);
|
|
393
|
+
cookieVal = "session=" + signed + "; Path=/; HttpOnly; SameSite=Lax";
|
|
394
|
+
}
|
|
395
|
+
|
|
376
396
|
return new Response(null, {
|
|
377
397
|
status: 302,
|
|
378
398
|
headers: {
|
|
379
399
|
"Location": "/profile",
|
|
380
|
-
"Set-Cookie":
|
|
381
|
-
user: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" }
|
|
382
|
-
})) + "; Path=/; HttpOnly; SameSite=Lax"
|
|
400
|
+
"Set-Cookie": cookieVal
|
|
383
401
|
}
|
|
384
402
|
});
|
|
385
403
|
}
|
|
386
404
|
|
|
387
405
|
if (url.pathname === "/api/auth/logout") {
|
|
406
|
+
let cookieVal = "__elm_ssr_session=; Path=/; Max-Age=0; HttpOnly";
|
|
407
|
+
if (options?.store && options?.secret) {
|
|
408
|
+
cookieVal = "session=; Path=/; Max-Age=0; HttpOnly";
|
|
409
|
+
const cookieHeader = request.headers.get("cookie");
|
|
410
|
+
if (cookieHeader) {
|
|
411
|
+
const sessionId = await readSignedCookie(cookieHeader, "session", options.secret);
|
|
412
|
+
if (sessionId) {
|
|
413
|
+
await options.store.delete(sessionId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
388
417
|
return new Response(null, {
|
|
389
418
|
status: 302,
|
|
390
419
|
headers: {
|
|
391
420
|
"Location": "/login",
|
|
392
|
-
"Set-Cookie":
|
|
421
|
+
"Set-Cookie": cookieVal
|
|
393
422
|
}
|
|
394
423
|
});
|
|
395
424
|
}
|
|
@@ -474,11 +503,13 @@ if (typeof Bun !== "undefined") {
|
|
|
474
503
|
}`;
|
|
475
504
|
|
|
476
505
|
let sessionsConfig = '';
|
|
506
|
+
let authInit = '';
|
|
477
507
|
if (auth) {
|
|
508
|
+
authInit = `\nexport const sessionStore = memorySessionStore();\n`;
|
|
478
509
|
sessionsConfig = `,
|
|
479
510
|
sessions: {
|
|
480
511
|
secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
|
|
481
|
-
store:
|
|
512
|
+
store: sessionStore,
|
|
482
513
|
secure: false
|
|
483
514
|
},
|
|
484
515
|
csrf: true`;
|
|
@@ -492,7 +523,8 @@ const baseWorkerFetch = worker.fetch;
|
|
|
492
523
|
worker.fetch = async (request, env, ctx) => {
|
|
493
524
|
const url = new URL(request.url);
|
|
494
525
|
if (url.pathname.startsWith("/api/auth/")) {
|
|
495
|
-
|
|
526
|
+
const secret = (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars";
|
|
527
|
+
return handleAuth(request, env, { store: sessionStore, secret });
|
|
496
528
|
}
|
|
497
529
|
return baseWorkerFetch(request, env, ctx);
|
|
498
530
|
};
|
|
@@ -582,7 +614,7 @@ export const createFlags = ({ request, path, formData, env }: { request?: Reques
|
|
|
582
614
|
|
|
583
615
|
export const renderPath = async (path: string) =>
|
|
584
616
|
renderApp(elmModule, createFlags({ path }));
|
|
585
|
-
${dbInit}
|
|
617
|
+
${dbInit}${authInit}
|
|
586
618
|
export const worker = createWorkerApp({
|
|
587
619
|
elmModule,
|
|
588
620
|
islands,
|
|
@@ -663,6 +695,7 @@ const filesForApp = (name, appRoot, options = {}) => {
|
|
|
663
695
|
const namespace = toPascalCase(name);
|
|
664
696
|
const db = options.db || !!options.auth;
|
|
665
697
|
const auth = options.auth;
|
|
698
|
+
const tailwind = options.tailwind;
|
|
666
699
|
|
|
667
700
|
const files = [
|
|
668
701
|
{ path: `${appRoot}/elm.json`, content: JSON.stringify(elmJsonTemplate(), null, 2) + "\n" },
|
|
@@ -710,12 +743,27 @@ SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32
|
|
|
710
743
|
});
|
|
711
744
|
}
|
|
712
745
|
|
|
746
|
+
if (tailwind) {
|
|
747
|
+
files.push({
|
|
748
|
+
path: `${appRoot}/src/app.css`,
|
|
749
|
+
content: `@tailwind base;
|
|
750
|
+
@tailwind components;
|
|
751
|
+
@tailwind utilities;
|
|
752
|
+
`
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const configEntry = {
|
|
757
|
+
name,
|
|
758
|
+
root: appRoot,
|
|
759
|
+
module: namespace
|
|
760
|
+
};
|
|
761
|
+
if (tailwind) {
|
|
762
|
+
configEntry.tailwind = true;
|
|
763
|
+
}
|
|
764
|
+
|
|
713
765
|
return {
|
|
714
|
-
configEntry
|
|
715
|
-
name,
|
|
716
|
-
root: appRoot,
|
|
717
|
-
module: namespace
|
|
718
|
-
},
|
|
766
|
+
configEntry,
|
|
719
767
|
files
|
|
720
768
|
};
|
|
721
769
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elm-ssr",
|
|
3
|
-
"version": "0.91.
|
|
3
|
+
"version": "0.91.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>",
|
package/src/app.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Middleware,
|
|
2
3
|
RenderFlagsFactory,
|
|
3
4
|
RouteCatalog,
|
|
4
5
|
WorkerExecutionContext,
|
|
@@ -50,6 +51,7 @@ export interface WorkerAppOptions {
|
|
|
50
51
|
* skip paths.
|
|
51
52
|
*/
|
|
52
53
|
csrf?: CsrfMiddlewareOptions | boolean;
|
|
54
|
+
middlewares?: Middleware[];
|
|
53
55
|
debug?: boolean;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -64,6 +66,7 @@ export const createWorkerApp = ({
|
|
|
64
66
|
log,
|
|
65
67
|
sessions,
|
|
66
68
|
csrf,
|
|
69
|
+
middlewares,
|
|
67
70
|
debug
|
|
68
71
|
}: WorkerAppOptions): WorkerHandler => {
|
|
69
72
|
const isDev = typeof process !== "undefined" && process.env ? (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") : false;
|
|
@@ -83,21 +86,24 @@ export const createWorkerApp = ({
|
|
|
83
86
|
debug: enableDebug
|
|
84
87
|
});
|
|
85
88
|
|
|
86
|
-
const
|
|
89
|
+
const middlewaresList = [
|
|
87
90
|
errorMiddleware,
|
|
88
91
|
requestIdMiddleware,
|
|
89
92
|
timingMiddleware,
|
|
90
93
|
loggingMiddleware(log)
|
|
91
94
|
];
|
|
92
95
|
if (sessions) {
|
|
93
|
-
|
|
96
|
+
middlewaresList.push(sessionMiddleware(sessions));
|
|
94
97
|
if (csrf) {
|
|
95
|
-
|
|
98
|
+
middlewaresList.push(csrfMiddleware(csrf === true ? {} : csrf));
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
middlewares
|
|
101
|
+
if (middlewares) {
|
|
102
|
+
middlewaresList.push(...middlewares);
|
|
103
|
+
}
|
|
104
|
+
middlewaresList.push(headMiddleware);
|
|
99
105
|
|
|
100
|
-
const appHandler = composeMiddleware(handler,
|
|
106
|
+
const appHandler = composeMiddleware(handler, middlewaresList);
|
|
101
107
|
|
|
102
108
|
return {
|
|
103
109
|
fetch(request: Request, env?: unknown, executionCtx?: WorkerExecutionContext) {
|
package/src/sessions/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ const appendSetCookie = (response: Response, header: string): Response => {
|
|
|
50
50
|
});
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const readSignedCookie = async (header: string | null, name: string, secret: string): Promise<string | null> => {
|
|
53
|
+
export const readSignedCookie = async (header: string | null, name: string, secret: string): Promise<string | null> => {
|
|
54
54
|
if (!header) {
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
@@ -104,14 +104,21 @@ export const sessionMiddleware = (options: SessionMiddlewareOptions): Middleware
|
|
|
104
104
|
if (sessionId) {
|
|
105
105
|
const existing = await options.store.get(sessionId);
|
|
106
106
|
if (existing) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (existing.expiresAt && Date.now() > existing.expiresAt) {
|
|
108
|
+
// Session expired
|
|
109
|
+
await options.store.delete(sessionId);
|
|
110
|
+
session = mintSession();
|
|
111
|
+
session.destroyed = true; // Ensure the cookie gets cleared as well
|
|
112
|
+
} else {
|
|
113
|
+
session = {
|
|
114
|
+
id: sessionId,
|
|
115
|
+
data: existing.data,
|
|
116
|
+
csrf: existing.csrf,
|
|
117
|
+
dirty: false,
|
|
118
|
+
destroyed: false,
|
|
119
|
+
isNew: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
115
122
|
} else {
|
|
116
123
|
session = mintSession();
|
|
117
124
|
}
|
package/src/sessions/store.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { CacheBackend } from "../backends";
|
|
|
2
2
|
import type { SessionRecord, SessionStore } from "./types";
|
|
3
3
|
|
|
4
4
|
/** In-memory session store. Survives only as long as the process. */
|
|
5
|
-
export const memorySessionStore = (initial?: Map<string, SessionRecord>): SessionStore => {
|
|
5
|
+
export const memorySessionStore = (initial?: Map<string, SessionRecord>): SessionStore & { store: Map<string, SessionRecord> } => {
|
|
6
6
|
const store = initial ?? new Map<string, SessionRecord>();
|
|
7
7
|
return {
|
|
8
|
+
store,
|
|
8
9
|
get: async (id) => {
|
|
9
10
|
const record = store.get(id);
|
|
10
11
|
if (!record) {
|