elm-ssr 0.91.2 → 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 +67 -23
- 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
|
}
|
|
@@ -415,11 +444,10 @@ const runtimeTemplate = (appRoot, db = false, auth = undefined) => {
|
|
|
415
444
|
`import ElmRuntime from "${generatedPrefix}/app.mjs";`
|
|
416
445
|
];
|
|
417
446
|
|
|
418
|
-
|
|
419
|
-
imports.push(`import { inMemoryEffects, cloudflareEffects } from "elm-ssr/effects";`);
|
|
420
|
-
}
|
|
447
|
+
imports.push(`import { inMemoryEffects, cloudflareEffects } from "elm-ssr/effects";`);
|
|
421
448
|
if (auth) {
|
|
422
449
|
imports.push(`import { handleAuth } from "./src/Endpoints/Auth";`);
|
|
450
|
+
imports.push(`import { memorySessionStore } from "elm-ssr/sessions";`);
|
|
423
451
|
}
|
|
424
452
|
|
|
425
453
|
let dbInit = '';
|
|
@@ -464,25 +492,24 @@ if (typeof Bun !== "undefined") {
|
|
|
464
492
|
},`;
|
|
465
493
|
}
|
|
466
494
|
|
|
467
|
-
|
|
468
|
-
if (db) {
|
|
469
|
-
effectsConfig = `,
|
|
495
|
+
const effectsConfig = `,
|
|
470
496
|
effects: (effect, context) => {
|
|
471
|
-
if (context.env
|
|
472
|
-
return cloudflareEffects({ dbBinding: "DB" })(effect, context);
|
|
497
|
+
if (context.env) {
|
|
498
|
+
return cloudflareEffects(${db ? '{ dbBinding: "DB" }' : ''})(effect, context);
|
|
473
499
|
}
|
|
474
500
|
return inMemoryEffects({
|
|
475
|
-
env: process.env as any
|
|
476
|
-
sql: sqlHandler
|
|
501
|
+
env: process.env as any${db ? ',\n sql: sqlHandler' : ''}
|
|
477
502
|
})(effect, context);
|
|
478
503
|
}`;
|
|
479
|
-
}
|
|
480
504
|
|
|
481
505
|
let sessionsConfig = '';
|
|
506
|
+
let authInit = '';
|
|
482
507
|
if (auth) {
|
|
508
|
+
authInit = `\nexport const sessionStore = memorySessionStore();\n`;
|
|
483
509
|
sessionsConfig = `,
|
|
484
510
|
sessions: {
|
|
485
511
|
secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
|
|
512
|
+
store: sessionStore,
|
|
486
513
|
secure: false
|
|
487
514
|
},
|
|
488
515
|
csrf: true`;
|
|
@@ -496,7 +523,8 @@ const baseWorkerFetch = worker.fetch;
|
|
|
496
523
|
worker.fetch = async (request, env, ctx) => {
|
|
497
524
|
const url = new URL(request.url);
|
|
498
525
|
if (url.pathname.startsWith("/api/auth/")) {
|
|
499
|
-
|
|
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 });
|
|
500
528
|
}
|
|
501
529
|
return baseWorkerFetch(request, env, ctx);
|
|
502
530
|
};
|
|
@@ -586,7 +614,7 @@ export const createFlags = ({ request, path, formData, env }: { request?: Reques
|
|
|
586
614
|
|
|
587
615
|
export const renderPath = async (path: string) =>
|
|
588
616
|
renderApp(elmModule, createFlags({ path }));
|
|
589
|
-
${dbInit}
|
|
617
|
+
${dbInit}${authInit}
|
|
590
618
|
export const worker = createWorkerApp({
|
|
591
619
|
elmModule,
|
|
592
620
|
islands,
|
|
@@ -667,6 +695,7 @@ const filesForApp = (name, appRoot, options = {}) => {
|
|
|
667
695
|
const namespace = toPascalCase(name);
|
|
668
696
|
const db = options.db || !!options.auth;
|
|
669
697
|
const auth = options.auth;
|
|
698
|
+
const tailwind = options.tailwind;
|
|
670
699
|
|
|
671
700
|
const files = [
|
|
672
701
|
{ path: `${appRoot}/elm.json`, content: JSON.stringify(elmJsonTemplate(), null, 2) + "\n" },
|
|
@@ -714,12 +743,27 @@ SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32
|
|
|
714
743
|
});
|
|
715
744
|
}
|
|
716
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
|
+
|
|
717
765
|
return {
|
|
718
|
-
configEntry
|
|
719
|
-
name,
|
|
720
|
-
root: appRoot,
|
|
721
|
-
module: namespace
|
|
722
|
-
},
|
|
766
|
+
configEntry,
|
|
723
767
|
files
|
|
724
768
|
};
|
|
725
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) {
|