elm-ssr 0.90.0 → 0.91.0
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 +30 -6
- package/lib/scaffold.mjs +288 -30
- package/package.json +1 -1
- package/src/app.ts +1 -1
- package/src/client-runtime/islands.ts +20 -1
- package/src/debugger.ts +148 -60
- package/src/http.ts +1 -0
- package/src/request-handler.ts +2 -1
- package/src/sessions/middleware.ts +12 -3
package/bin/elm-ssr.mjs
CHANGED
|
@@ -77,8 +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
|
-
|
|
81
|
-
|
|
80
|
+
(use --db to wire SQLite/migrations, --auth betterAuth|auth0 for auth guards)
|
|
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
83
|
routes Print configured apps and their public modules
|
|
83
84
|
route <path> Scaffold a new route (standard HTML, --api JSON, --ws WebSocket, or --sse EventSource)
|
|
84
85
|
query Generate type-safe Elm Db modules from SQL table definitions
|
|
@@ -195,11 +196,22 @@ switch (command) {
|
|
|
195
196
|
const name = args[1];
|
|
196
197
|
|
|
197
198
|
if (!name) {
|
|
198
|
-
console.error("Usage: elm-ssr init <name>");
|
|
199
|
+
console.error("Usage: elm-ssr init <name> [--db] [--auth betterAuth|auth0]");
|
|
199
200
|
process.exit(1);
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
const
|
|
203
|
+
const db = args.includes("--db");
|
|
204
|
+
let auth = findFlagValue("--auth");
|
|
205
|
+
if (auth) {
|
|
206
|
+
if (auth === "better-auth" || auth === "betterAuth") {
|
|
207
|
+
auth = "better-auth";
|
|
208
|
+
} else if (auth !== "auth0") {
|
|
209
|
+
console.error("Error: --auth only supports 'betterAuth' or 'auth0'");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const created = await createAppScaffold(rootPath, name, { root: ".", db, auth });
|
|
203
215
|
console.log(`Initialized ${created.name} in current directory`);
|
|
204
216
|
break;
|
|
205
217
|
}
|
|
@@ -208,7 +220,7 @@ switch (command) {
|
|
|
208
220
|
const name = args[1];
|
|
209
221
|
|
|
210
222
|
if (!name) {
|
|
211
|
-
console.error("Usage: elm-ssr new <name> [--in <subdir>]");
|
|
223
|
+
console.error("Usage: elm-ssr new <name> [--in <subdir>] [--db] [--auth betterAuth|auth0]");
|
|
212
224
|
console.error(" Default location: <workspace>/<name>/");
|
|
213
225
|
console.error(" Use --in apps to place it under <workspace>/apps/<name>/, etc.");
|
|
214
226
|
process.exit(1);
|
|
@@ -216,7 +228,19 @@ switch (command) {
|
|
|
216
228
|
|
|
217
229
|
const subdir = findFlagValue("--in");
|
|
218
230
|
const appRoot = subdir ? `${subdir.replace(/\/+$/, "")}/${name}` : name;
|
|
219
|
-
|
|
231
|
+
|
|
232
|
+
const db = args.includes("--db");
|
|
233
|
+
let auth = findFlagValue("--auth");
|
|
234
|
+
if (auth) {
|
|
235
|
+
if (auth === "better-auth" || auth === "betterAuth") {
|
|
236
|
+
auth = "better-auth";
|
|
237
|
+
} else if (auth !== "auth0") {
|
|
238
|
+
console.error("Error: --auth only supports 'betterAuth' or 'auth0'");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const created = await createAppScaffold(rootPath, name, { root: appRoot, db, auth });
|
|
220
244
|
console.log(`Created ${created.name} at ${created.root}`);
|
|
221
245
|
break;
|
|
222
246
|
}
|
package/lib/scaffold.mjs
CHANGED
|
@@ -85,7 +85,10 @@ import ${namespace}.View.Shared as Shared
|
|
|
85
85
|
|
|
86
86
|
page : Request -> Loader (Document Never)
|
|
87
87
|
page _ =
|
|
88
|
-
Loader.
|
|
88
|
+
Loader.env "GREETING"
|
|
89
|
+
|> Loader.map (\\maybeGreeting ->
|
|
90
|
+
view (Maybe.withDefault "Hello from default env!" maybeGreeting)
|
|
91
|
+
)
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
action : Request -> Action (Document Never)
|
|
@@ -93,14 +96,15 @@ action _ =
|
|
|
93
96
|
Action.fail 405 "Method not allowed"
|
|
94
97
|
|
|
95
98
|
|
|
96
|
-
view : Document Never
|
|
97
|
-
view =
|
|
99
|
+
view : String -> Document Never
|
|
100
|
+
view greeting =
|
|
98
101
|
Page.page
|
|
99
102
|
{ title = "Starter | elm-ssr"
|
|
100
103
|
, head = Shared.head
|
|
101
104
|
, body =
|
|
102
105
|
[ Shared.shell "elm-ssr starter"
|
|
103
|
-
[ p [] [ text "
|
|
106
|
+
[ p [] [ text ("Greeting from environment: " ++ greeting) ]
|
|
107
|
+
, p [] [ text "This page is stateless and renders on the edge with no client runtime." ]
|
|
104
108
|
, p [] [ a [ class "link", href "/counter" ] [ text "Open the interactive counter" ] ]
|
|
105
109
|
]
|
|
106
110
|
]
|
|
@@ -272,19 +276,210 @@ view =
|
|
|
272
276
|
}
|
|
273
277
|
`;
|
|
274
278
|
|
|
275
|
-
const
|
|
279
|
+
const loginRouteTemplate = (namespace, authProvider) => `module ${namespace}.Routes.Login exposing (page, action)
|
|
280
|
+
|
|
281
|
+
import ElmSsr.Action as Action exposing (Action)
|
|
282
|
+
import ElmSsr.Document exposing (Document)
|
|
283
|
+
import ElmSsr.Html exposing (a, div, p, text)
|
|
284
|
+
import ElmSsr.Html.Attributes as Attr
|
|
285
|
+
import ElmSsr.Loader as Loader exposing (Loader)
|
|
286
|
+
import ElmSsr.Page as Page
|
|
287
|
+
import ElmSsr.Route exposing (Request)
|
|
288
|
+
import ${namespace}.View.Shared as Shared
|
|
289
|
+
|
|
290
|
+
page : Request -> Loader (Document Never)
|
|
291
|
+
page _ =
|
|
292
|
+
Loader.succeed view
|
|
293
|
+
|
|
294
|
+
action : Request -> Action (Document Never)
|
|
295
|
+
action _ =
|
|
296
|
+
Action.fail 405 "Method not allowed"
|
|
297
|
+
|
|
298
|
+
view : Document Never
|
|
299
|
+
view =
|
|
300
|
+
Page.page
|
|
301
|
+
{ title = "Login | elm-ssr"
|
|
302
|
+
, head = Shared.head
|
|
303
|
+
, body =
|
|
304
|
+
[ Shared.shell "Sign In"
|
|
305
|
+
[ div [ Attr.class "login-container" ]
|
|
306
|
+
[ p [] [ text "Access user authentication example (${authProvider})." ]
|
|
307
|
+
, div [ Attr.style "margin-top" "20px" ]
|
|
308
|
+
[ a
|
|
309
|
+
[ Attr.class "button primary"
|
|
310
|
+
, Attr.href "/api/auth/login"
|
|
311
|
+
]
|
|
312
|
+
[ text "Sign In with Auth Provider" ]
|
|
313
|
+
]
|
|
314
|
+
]
|
|
315
|
+
]
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
`;
|
|
319
|
+
|
|
320
|
+
const profileRouteTemplate = (namespace) => `module ${namespace}.Routes.Profile exposing (page, action)
|
|
321
|
+
|
|
322
|
+
import ElmSsr.Action as Action exposing (Action)
|
|
323
|
+
import ElmSsr.Document exposing (Document)
|
|
324
|
+
import ElmSsr.Html exposing (a, div, h2, p, text)
|
|
325
|
+
import ElmSsr.Html.Attributes as Attr
|
|
326
|
+
import ElmSsr.Loader as Loader exposing (Loader)
|
|
327
|
+
import ElmSsr.Page as Page
|
|
328
|
+
import ElmSsr.Route as Route exposing (Request)
|
|
329
|
+
import ${namespace}.View.Shared as Shared
|
|
330
|
+
import Json.Decode as Decode
|
|
331
|
+
|
|
332
|
+
type alias UserProfile =
|
|
333
|
+
{ email : String
|
|
334
|
+
, name : Maybe String
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
userDecoder : Decode.Decoder UserProfile
|
|
338
|
+
userDecoder =
|
|
339
|
+
Decode.map2 UserProfile
|
|
340
|
+
(Decode.field "email" Decode.string)
|
|
341
|
+
(Decode.maybe (Decode.field "name" Decode.string))
|
|
342
|
+
|
|
343
|
+
page : Request -> Loader (Document Never)
|
|
344
|
+
page request =
|
|
345
|
+
Loader.requireUser userDecoder "/login" request
|
|
346
|
+
|> Loader.map view
|
|
347
|
+
|
|
348
|
+
action : Request -> Action (Document Never)
|
|
349
|
+
action _ =
|
|
350
|
+
Action.fail 405 "Method not allowed"
|
|
351
|
+
|
|
352
|
+
view : UserProfile -> Document Never
|
|
353
|
+
view user =
|
|
354
|
+
Page.page
|
|
355
|
+
{ title = "Profile | elm-ssr"
|
|
356
|
+
, head = Shared.head
|
|
357
|
+
, body =
|
|
358
|
+
[ Shared.shell "User Profile"
|
|
359
|
+
[ div []
|
|
360
|
+
[ h2 [] [ text ("Welcome, " ++ (Maybe.withDefault user.email user.name)) ]
|
|
361
|
+
, p [] [ text ("Email: " ++ user.email) ]
|
|
362
|
+
, p [ Attr.style "margin-top" "20px" ]
|
|
363
|
+
[ a [ Attr.class "button", Attr.href "/api/auth/logout" ] [ text "Sign Out" ] ]
|
|
364
|
+
]
|
|
365
|
+
]
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
const authEndpointTemplate = (authProvider) => `export const handleAuth = async (request: Request, env: any): Promise<Response> => {
|
|
371
|
+
const url = new URL(request.url);
|
|
372
|
+
|
|
373
|
+
if (url.pathname === "/api/auth/login") {
|
|
374
|
+
// Mock login session update for ${authProvider}
|
|
375
|
+
return new Response(null, {
|
|
376
|
+
status: 302,
|
|
377
|
+
headers: {
|
|
378
|
+
"Location": "/profile",
|
|
379
|
+
"Set-Cookie": "__elm_ssr_session=" + encodeURIComponent(JSON.stringify({
|
|
380
|
+
user: { email: "user@example.com", name: "${authProvider === "better-auth" ? "BetterAuth User" : "Auth0 User"}" }
|
|
381
|
+
})) + "; Path=/; HttpOnly; SameSite=Lax"
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (url.pathname === "/api/auth/logout") {
|
|
387
|
+
return new Response(null, {
|
|
388
|
+
status: 302,
|
|
389
|
+
headers: {
|
|
390
|
+
"Location": "/login",
|
|
391
|
+
"Set-Cookie": "__elm_ssr_session=; Path=/; Max-Age=0; HttpOnly"
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return new Response("${authProvider} Endpoint API Mock", { status: 200 });
|
|
397
|
+
};
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
const runtimeTemplate = (appRoot, db = false, auth = undefined) => {
|
|
276
401
|
// appRoot is a slash-separated path like "my-app" or "apps/my-app".
|
|
277
402
|
// The generated bundles live at <workspaceRoot>/generated/<appRoot>/. From
|
|
278
403
|
// <workspaceRoot>/<appRoot>/runtime.ts, we climb out by one ".." per segment.
|
|
279
404
|
const upToRoot = appRoot === "." ? "." : appRoot.split("/").map(() => "..").join("/");
|
|
280
405
|
const generatedPrefix = `${upToRoot}/generated/${appRoot === "." ? "" : appRoot}`.replace(/\/+$/, "");
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
import
|
|
284
|
-
import {
|
|
285
|
-
import {
|
|
286
|
-
|
|
287
|
-
import
|
|
406
|
+
|
|
407
|
+
const imports = [
|
|
408
|
+
`import { createWorkerApp } from "elm-ssr";`,
|
|
409
|
+
`import { renderApp, type CompiledElmModule } from "elm-ssr/render";`,
|
|
410
|
+
`import type { RouteCatalog } from "elm-ssr/http";`,
|
|
411
|
+
`import { islands, bundleSource } from "${generatedPrefix}/islands-manifest";`,
|
|
412
|
+
`import { stylesheet } from "./styles";`,
|
|
413
|
+
`// @ts-expect-error Generated at build time.`,
|
|
414
|
+
`import ElmRuntime from "${generatedPrefix}/app.mjs";`
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
if (db) {
|
|
418
|
+
imports.push(`import { Database } from "bun:sqlite";`);
|
|
419
|
+
imports.push(`import { inMemoryEffects } from "elm-ssr/effects";`);
|
|
420
|
+
}
|
|
421
|
+
if (auth) {
|
|
422
|
+
imports.push(`import { handleAuth } from "./src/Endpoints/Auth";`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let dbInit = '';
|
|
426
|
+
if (db) {
|
|
427
|
+
dbInit = `\nconst db = new Database("app.db");\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let routeAuthAdditions = '';
|
|
431
|
+
if (auth) {
|
|
432
|
+
routeAuthAdditions = `
|
|
433
|
+
{
|
|
434
|
+
path: "/login",
|
|
435
|
+
methods: ["GET"],
|
|
436
|
+
description: "User authentication login page."
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
path: "/profile",
|
|
440
|
+
methods: ["GET"],
|
|
441
|
+
description: "Authenticated user profile."
|
|
442
|
+
},`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let effectsConfig = '';
|
|
446
|
+
if (db) {
|
|
447
|
+
effectsConfig = `,
|
|
448
|
+
effects: inMemoryEffects({
|
|
449
|
+
env: process.env as any,
|
|
450
|
+
sql: (sql, params) => {
|
|
451
|
+
const query = db.prepare(sql);
|
|
452
|
+
return query.all(...params);
|
|
453
|
+
}
|
|
454
|
+
})`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let sessionsConfig = '';
|
|
458
|
+
if (auth) {
|
|
459
|
+
sessionsConfig = `,
|
|
460
|
+
sessions: {
|
|
461
|
+
secret: (env) => (env?.SESSION_SECRET as string) || "change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars",
|
|
462
|
+
secure: false
|
|
463
|
+
},
|
|
464
|
+
csrf: true`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let authIntercept = '';
|
|
468
|
+
if (auth) {
|
|
469
|
+
authIntercept = `
|
|
470
|
+
// Intercept Auth Provider requests
|
|
471
|
+
const baseWorkerFetch = worker.fetch;
|
|
472
|
+
worker.fetch = async (request, env, ctx) => {
|
|
473
|
+
const url = new URL(request.url);
|
|
474
|
+
if (url.pathname.startsWith("/api/auth/")) {
|
|
475
|
+
return handleAuth(request, env);
|
|
476
|
+
}
|
|
477
|
+
return baseWorkerFetch(request, env, ctx);
|
|
478
|
+
};
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return `${imports.join("\n")}
|
|
288
483
|
|
|
289
484
|
const elmModule = ElmRuntime as CompiledElmModule;
|
|
290
485
|
|
|
@@ -299,7 +494,7 @@ export const routes: RouteCatalog = {
|
|
|
299
494
|
path: "/counter",
|
|
300
495
|
methods: ["GET", "HEAD"],
|
|
301
496
|
description: "Interactive counter route rendered from Elm."
|
|
302
|
-
}
|
|
497
|
+
},${routeAuthAdditions}
|
|
303
498
|
],
|
|
304
499
|
assets: [
|
|
305
500
|
{
|
|
@@ -344,28 +539,39 @@ export const routes: RouteCatalog = {
|
|
|
344
539
|
]
|
|
345
540
|
};
|
|
346
541
|
|
|
347
|
-
export const createFlags = ({ request, path, formData }: { request?: Request; url?: URL; path: string; formData?: Record<string, string> }) => {
|
|
542
|
+
export const createFlags = ({ request, path, formData, env }: { request?: Request; url?: URL; path: string; formData?: Record<string, string>; env?: Record<string, unknown> }) => {
|
|
348
543
|
const [pathname, search = ""] = path.split("?");
|
|
349
544
|
|
|
545
|
+
const envVars: Record<string, string | number | boolean> = {};
|
|
546
|
+
if (env) {
|
|
547
|
+
for (const [key, value] of Object.entries(env)) {
|
|
548
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
549
|
+
envVars[key] = value;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
350
554
|
return {
|
|
351
555
|
method: request?.method ?? "GET",
|
|
352
556
|
path: pathname,
|
|
353
557
|
query: Object.fromEntries(new URLSearchParams(search)),
|
|
354
|
-
formData: formData ?? {}
|
|
558
|
+
formData: formData ?? {},
|
|
559
|
+
env: envVars
|
|
355
560
|
};
|
|
356
561
|
};
|
|
357
562
|
|
|
358
563
|
export const renderPath = async (path: string) =>
|
|
359
564
|
renderApp(elmModule, createFlags({ path }));
|
|
360
|
-
|
|
565
|
+
${dbInit}
|
|
361
566
|
export const worker = createWorkerApp({
|
|
362
567
|
elmModule,
|
|
363
568
|
islands,
|
|
364
569
|
islandsBundle: bundleSource,
|
|
365
570
|
stylesheet,
|
|
366
571
|
routes,
|
|
367
|
-
createFlags
|
|
572
|
+
createFlags${sessionsConfig}${effectsConfig}
|
|
368
573
|
});
|
|
574
|
+
${authIntercept}
|
|
369
575
|
`;
|
|
370
576
|
};
|
|
371
577
|
|
|
@@ -433,8 +639,56 @@ body {
|
|
|
433
639
|
\`;
|
|
434
640
|
`;
|
|
435
641
|
|
|
436
|
-
const filesForApp = (name, appRoot) => {
|
|
642
|
+
const filesForApp = (name, appRoot, options = {}) => {
|
|
437
643
|
const namespace = toPascalCase(name);
|
|
644
|
+
const db = options.db || !!options.auth;
|
|
645
|
+
const auth = options.auth;
|
|
646
|
+
|
|
647
|
+
const files = [
|
|
648
|
+
{ path: `${appRoot}/elm.json`, content: JSON.stringify(elmJsonTemplate(), null, 2) + "\n" },
|
|
649
|
+
{ path: `${appRoot}/runtime.ts`, content: runtimeTemplate(appRoot, db, auth) },
|
|
650
|
+
{ path: `${appRoot}/worker.ts`, content: workerTemplate() },
|
|
651
|
+
{ path: `${appRoot}/styles.ts`, content: stylesTemplate() },
|
|
652
|
+
{ path: `${appRoot}/src/${namespace}/View/Shared.elm`, content: sharedTemplate(namespace) },
|
|
653
|
+
{ path: `${appRoot}/src/${namespace}/Routes/Index.elm`, content: indexRouteTemplate(namespace) },
|
|
654
|
+
{ path: `${appRoot}/src/${namespace}/Routes/Counter.elm`, content: counterRouteTemplate(namespace) },
|
|
655
|
+
{ path: `${appRoot}/src/${namespace}/Routes/NotFound.elm`, content: notFoundRouteTemplate(namespace) },
|
|
656
|
+
{ path: `${appRoot}/src/${namespace}/Islands/Counter.elm`, content: counterIslandTemplate(namespace) },
|
|
657
|
+
{
|
|
658
|
+
path: `${appRoot}/.dev.vars`,
|
|
659
|
+
content: `GREETING="Hello from your local .dev.vars file!"
|
|
660
|
+
SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars"
|
|
661
|
+
`
|
|
662
|
+
}
|
|
663
|
+
];
|
|
664
|
+
|
|
665
|
+
if (db) {
|
|
666
|
+
files.push({
|
|
667
|
+
path: `${appRoot}/migrations/0001_init.sql`,
|
|
668
|
+
content: `CREATE TABLE IF NOT EXISTS users (
|
|
669
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
670
|
+
email TEXT NOT NULL UNIQUE,
|
|
671
|
+
name TEXT,
|
|
672
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
673
|
+
);
|
|
674
|
+
`
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (auth) {
|
|
679
|
+
files.push({
|
|
680
|
+
path: `${appRoot}/src/${namespace}/Routes/Login.elm`,
|
|
681
|
+
content: loginRouteTemplate(namespace, auth)
|
|
682
|
+
});
|
|
683
|
+
files.push({
|
|
684
|
+
path: `${appRoot}/src/${namespace}/Routes/Profile.elm`,
|
|
685
|
+
content: profileRouteTemplate(namespace)
|
|
686
|
+
});
|
|
687
|
+
files.push({
|
|
688
|
+
path: `${appRoot}/src/Endpoints/Auth.ts`,
|
|
689
|
+
content: authEndpointTemplate(auth)
|
|
690
|
+
});
|
|
691
|
+
}
|
|
438
692
|
|
|
439
693
|
return {
|
|
440
694
|
configEntry: {
|
|
@@ -442,17 +696,7 @@ const filesForApp = (name, appRoot) => {
|
|
|
442
696
|
root: appRoot,
|
|
443
697
|
module: namespace
|
|
444
698
|
},
|
|
445
|
-
files
|
|
446
|
-
{ path: `${appRoot}/elm.json`, content: JSON.stringify(elmJsonTemplate(), null, 2) + "\n" },
|
|
447
|
-
{ path: `${appRoot}/runtime.ts`, content: runtimeTemplate(appRoot) },
|
|
448
|
-
{ path: `${appRoot}/worker.ts`, content: workerTemplate() },
|
|
449
|
-
{ path: `${appRoot}/styles.ts`, content: stylesTemplate() },
|
|
450
|
-
{ path: `${appRoot}/src/${namespace}/View/Shared.elm`, content: sharedTemplate(namespace) },
|
|
451
|
-
{ path: `${appRoot}/src/${namespace}/Routes/Index.elm`, content: indexRouteTemplate(namespace) },
|
|
452
|
-
{ path: `${appRoot}/src/${namespace}/Routes/Counter.elm`, content: counterRouteTemplate(namespace) },
|
|
453
|
-
{ path: `${appRoot}/src/${namespace}/Routes/NotFound.elm`, content: notFoundRouteTemplate(namespace) },
|
|
454
|
-
{ path: `${appRoot}/src/${namespace}/Islands/Counter.elm`, content: counterIslandTemplate(namespace) }
|
|
455
|
-
]
|
|
699
|
+
files
|
|
456
700
|
};
|
|
457
701
|
};
|
|
458
702
|
|
|
@@ -521,7 +765,7 @@ export const createAppScaffold = async (rootPath, name, options = {}) => {
|
|
|
521
765
|
|
|
522
766
|
const appRoot = normalizeAppRoot(options.root, name);
|
|
523
767
|
|
|
524
|
-
const { configEntry, files } = filesForApp(name, appRoot);
|
|
768
|
+
const { configEntry, files } = filesForApp(name, appRoot, options);
|
|
525
769
|
|
|
526
770
|
for (const file of files) {
|
|
527
771
|
const filePath = resolve(rootPath, file.path);
|
|
@@ -529,6 +773,20 @@ export const createAppScaffold = async (rootPath, name, options = {}) => {
|
|
|
529
773
|
await writeFile(filePath, file.content, "utf8");
|
|
530
774
|
}
|
|
531
775
|
|
|
776
|
+
// Create .env at workspace root if it doesn't exist
|
|
777
|
+
const envPath = resolve(rootPath, ".env");
|
|
778
|
+
let envExists = false;
|
|
779
|
+
try {
|
|
780
|
+
await stat(envPath);
|
|
781
|
+
envExists = true;
|
|
782
|
+
} catch {}
|
|
783
|
+
|
|
784
|
+
if (!envExists) {
|
|
785
|
+
await writeFile(envPath, `GREETING="Hello from your local .env file!"
|
|
786
|
+
SESSION_SECRET="change-me-to-a-secure-random-hmac-secret-key-that-is-at-least-32-chars"
|
|
787
|
+
`, "utf8");
|
|
788
|
+
}
|
|
789
|
+
|
|
532
790
|
await writeWorkspaceConfig(rootPath, {
|
|
533
791
|
...config,
|
|
534
792
|
apps: [...config.apps, configEntry]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elm-ssr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0",
|
|
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
|
@@ -107,7 +107,7 @@ export const createWorkerApp = ({
|
|
|
107
107
|
requestId: "",
|
|
108
108
|
startedAt: performance.now(),
|
|
109
109
|
executionCtx,
|
|
110
|
-
env: (env ?? undefined) as Record<string, unknown> | undefined
|
|
110
|
+
env: (env ?? (typeof process !== "undefined" ? process.env : undefined)) as Record<string, unknown> | undefined
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
};
|
|
@@ -27,7 +27,26 @@ function createIslandsRuntime(deps) {
|
|
|
27
27
|
|
|
28
28
|
if (app.ports.broadcastOut) {
|
|
29
29
|
app.ports.broadcastOut.subscribe((event) => {
|
|
30
|
-
|
|
30
|
+
const detail = {
|
|
31
|
+
...event,
|
|
32
|
+
__elmssr_origin: {
|
|
33
|
+
name: marker.getAttribute("data-elmssr-island"),
|
|
34
|
+
id: marker.getAttribute("data-elmssr-id")
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
window.dispatchEvent(new window.CustomEvent("elm-ssr-broadcast", { detail }));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (app.ports.stateOut) {
|
|
42
|
+
app.ports.stateOut.subscribe((state) => {
|
|
43
|
+
window.dispatchEvent(new window.CustomEvent("elm-ssr-state-update", {
|
|
44
|
+
detail: {
|
|
45
|
+
name: marker.getAttribute("data-elmssr-island"),
|
|
46
|
+
id: marker.getAttribute("data-elmssr-id"),
|
|
47
|
+
state
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
31
50
|
});
|
|
32
51
|
}
|
|
33
52
|
|
package/src/debugger.ts
CHANGED
|
@@ -235,38 +235,51 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
235
235
|
toggleBtn.id = 'elm-ssr-debug-toggle';
|
|
236
236
|
toggleBtn.innerHTML = '⚡ Debug';
|
|
237
237
|
document.body.appendChild(toggleBtn);
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
// 4. Create Panel DOM
|
|
240
240
|
const panel = document.createElement('div');
|
|
241
241
|
panel.id = 'elm-ssr-debug-panel';
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
</div>
|
|
260
|
-
\${sqlSnippet}
|
|
261
|
-
\${paramsSnippet}
|
|
262
|
-
\${detailError}
|
|
243
|
+
const isSqlKind = (kind) => ['query', 'queryOne', 'execute'].includes(kind);
|
|
244
|
+
|
|
245
|
+
const generateEffectItemHtml = (eff) => {
|
|
246
|
+
const isSql = isSqlKind(eff.kind);
|
|
247
|
+
const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
|
|
248
|
+
const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
|
|
249
|
+
\`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
|
|
250
|
+
const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
|
|
251
|
+
const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
|
|
252
|
+
return \`
|
|
253
|
+
<li class="debug-item">
|
|
254
|
+
<div class="debug-item-meta" style="flex: 1;">
|
|
255
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
256
|
+
<span class="debug-badge badge-primary">\${eff.kind}</span>
|
|
257
|
+
\${outcome}
|
|
258
|
+
<span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
|
|
263
259
|
</div>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
\${sqlSnippet}
|
|
261
|
+
\${paramsSnippet}
|
|
262
|
+
\${detailError}
|
|
263
|
+
</div>
|
|
264
|
+
</li>
|
|
265
|
+
\`;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getDbHtml = (effects) => {
|
|
269
|
+
const dbEffects = (effects || []).filter(eff => isSqlKind(eff.kind));
|
|
270
|
+
if (dbEffects.length === 0) {
|
|
271
|
+
return '<div style="color: #9ca3af; text-align: center; padding: 20px;">No SQL database queries executed for this request.</div>';
|
|
272
|
+
}
|
|
273
|
+
return \`<ul class="debug-list">\${dbEffects.map(generateEffectItemHtml).join('')}</ul>\`;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const getEffectsHtml = (effects) => {
|
|
277
|
+
const nonDbEffects = (effects || []).filter(eff => !isSqlKind(eff.kind));
|
|
278
|
+
if (nonDbEffects.length === 0) {
|
|
279
|
+
return '<div style="color: #9ca3af; text-align: center; padding: 20px;">No other server-side effects executed for this request.</div>';
|
|
280
|
+
}
|
|
281
|
+
return \`<ul class="debug-list">\${nonDbEffects.map(generateEffectItemHtml).join('')}</ul>\`;
|
|
282
|
+
};
|
|
270
283
|
|
|
271
284
|
panel.innerHTML = \`
|
|
272
285
|
<div class="debug-header">
|
|
@@ -276,6 +289,7 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
276
289
|
<div class="debug-tabs">
|
|
277
290
|
<button class="debug-tab active" data-tab="overview">Overview</button>
|
|
278
291
|
<button class="debug-tab" data-tab="islands">Islands</button>
|
|
292
|
+
<button class="debug-tab" data-tab="database">Database</button>
|
|
279
293
|
<button class="debug-tab" data-tab="effects">Effects</button>
|
|
280
294
|
<button class="debug-tab" data-tab="session">Session</button>
|
|
281
295
|
<button class="debug-tab" data-tab="bridges">Bridges</button>
|
|
@@ -313,11 +327,14 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
313
327
|
</ul>
|
|
314
328
|
</div>
|
|
315
329
|
|
|
330
|
+
<!-- DATABASE TAB -->
|
|
331
|
+
<div class="debug-pane" id="pane-database">
|
|
332
|
+
\${getDbHtml(data.effects)}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
316
335
|
<!-- EFFECTS TAB -->
|
|
317
336
|
<div class="debug-pane" id="pane-effects">
|
|
318
|
-
|
|
319
|
-
\${effectsHtml}
|
|
320
|
-
</ul>
|
|
337
|
+
\${getEffectsHtml(data.effects)}
|
|
321
338
|
</div>
|
|
322
339
|
|
|
323
340
|
<!-- SESSION TAB -->
|
|
@@ -379,6 +396,48 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
379
396
|
highlight.style.display = 'none';
|
|
380
397
|
};
|
|
381
398
|
|
|
399
|
+
const islandMutations = new Map();
|
|
400
|
+
const islandActiveStates = new Map();
|
|
401
|
+
const activeObservers = new Map();
|
|
402
|
+
|
|
403
|
+
const setupMutationObservers = () => {
|
|
404
|
+
for (const obs of activeObservers.values()) {
|
|
405
|
+
obs.disconnect();
|
|
406
|
+
}
|
|
407
|
+
activeObservers.clear();
|
|
408
|
+
|
|
409
|
+
const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
|
|
410
|
+
markers.forEach(marker => {
|
|
411
|
+
if (!islandMutations.has(marker)) {
|
|
412
|
+
islandMutations.set(marker, { count: 0, lastUpdate: null });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const observer = new MutationObserver(() => {
|
|
416
|
+
const stats = islandMutations.get(marker);
|
|
417
|
+
stats.count += 1;
|
|
418
|
+
stats.lastUpdate = new Date();
|
|
419
|
+
|
|
420
|
+
const activeTab = panel.querySelector('.debug-tab.active')?.getAttribute('data-tab');
|
|
421
|
+
if (activeTab === 'islands') {
|
|
422
|
+
scanIslands();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
observer.observe(marker, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
427
|
+
activeObservers.set(marker, observer);
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const findMarkerByOrigin = (origin) => {
|
|
432
|
+
if (!origin) return null;
|
|
433
|
+
const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
|
|
434
|
+
return markers.find(marker => {
|
|
435
|
+
const name = marker.getAttribute('data-elmssr-island');
|
|
436
|
+
const id = marker.getAttribute('data-elmssr-id');
|
|
437
|
+
return name === origin.name && (id === origin.id || (!id && !origin.id));
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
|
|
382
441
|
const scanIslands = () => {
|
|
383
442
|
const list = panel.querySelector('#debug-islands-list');
|
|
384
443
|
const markers = Array.from(document.getElementsByTagName('elm-ssr-island'));
|
|
@@ -395,6 +454,27 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
395
454
|
const isBooted = marker.getAttribute('data-elmssr-booted') === 'true';
|
|
396
455
|
const id = marker.getAttribute('data-elmssr-id') || 'none';
|
|
397
456
|
|
|
457
|
+
const stats = islandMutations.get(marker) || { count: 0, lastUpdate: null };
|
|
458
|
+
const lastUpdateStr = stats.lastUpdate
|
|
459
|
+
? \`Last active DOM mutation: \${stats.lastUpdate.toLocaleTimeString()}\`
|
|
460
|
+
: 'No client state changes recorded';
|
|
461
|
+
|
|
462
|
+
const activeState = islandActiveStates.get(marker);
|
|
463
|
+
const domTextPreview = marker.textContent.trim().replace(/\\s+/g, ' ');
|
|
464
|
+
|
|
465
|
+
let stateSnippet = '';
|
|
466
|
+
if (activeState) {
|
|
467
|
+
stateSnippet = \`
|
|
468
|
+
<div style="font-size: 0.85em; margin-top: 6px; color: #a5b4fc;"><strong>Active Model State:</strong></div>
|
|
469
|
+
<pre class="debug-code" style="font-size: 0.85em; max-height: 120px; overflow-y: auto; color: #818cf8;">\${JSON.stringify(activeState, null, 2)}</pre>
|
|
470
|
+
\`;
|
|
471
|
+
} else if (domTextPreview) {
|
|
472
|
+
stateSnippet = \`
|
|
473
|
+
<div style="font-size: 0.85em; margin-top: 6px; color: #9ca3af;"><strong>DOM Text Preview (Live):</strong></div>
|
|
474
|
+
<div class="debug-code" style="font-size: 0.85em; color: #9ca3af; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\${domTextPreview}</div>
|
|
475
|
+
\`;
|
|
476
|
+
}
|
|
477
|
+
|
|
398
478
|
const li = document.createElement('li');
|
|
399
479
|
li.className = 'debug-item';
|
|
400
480
|
li.style.cursor = 'pointer';
|
|
@@ -406,9 +486,13 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
406
486
|
\${isBooted ? 'BOOTED' : 'INITIALIZING'}
|
|
407
487
|
</span>
|
|
408
488
|
</div>
|
|
409
|
-
<div style="font-size: 0.8em; margin-top: 4px; color: #9ca3af;">
|
|
489
|
+
<div style="font-size: 0.8em; margin-top: 4px; color: #9ca3af; display: flex; justify-content: space-between;">
|
|
410
490
|
<span><strong>ID:</strong> \${id}</span>
|
|
491
|
+
<span style="color: #34d399;"><strong>DOM Mutations:</strong> \${stats.count}</span>
|
|
411
492
|
</div>
|
|
493
|
+
<div style="font-size: 0.75em; color: #9ca3af; margin-top: 2px;">\${lastUpdateStr}</div>
|
|
494
|
+
\${stateSnippet}
|
|
495
|
+
<div style="font-size: 0.8em; margin-top: 6px; color: #e5e7eb;"><strong>Initial Props:</strong></div>
|
|
412
496
|
<pre class="debug-code" style="font-size: 0.85em; max-height: 80px; overflow-y: auto;">\${JSON.stringify(JSON.parse(props), null, 2)}</pre>
|
|
413
497
|
</div>
|
|
414
498
|
\`;
|
|
@@ -420,6 +504,21 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
420
504
|
};
|
|
421
505
|
|
|
422
506
|
panel.querySelector('[data-tab="islands"]').addEventListener('click', scanIslands);
|
|
507
|
+
setupMutationObservers();
|
|
508
|
+
|
|
509
|
+
// Listen to explicit state update ports
|
|
510
|
+
window.addEventListener('elm-ssr-state-update', (event) => {
|
|
511
|
+
if (event.detail) {
|
|
512
|
+
const marker = findMarkerByOrigin(event.detail);
|
|
513
|
+
if (marker) {
|
|
514
|
+
islandActiveStates.set(marker, event.detail.state);
|
|
515
|
+
const activeTab = panel.querySelector('.debug-tab.active')?.getAttribute('data-tab');
|
|
516
|
+
if (activeTab === 'islands') {
|
|
517
|
+
scanIslands();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
423
522
|
|
|
424
523
|
const bridgeLog = panel.querySelector('#debug-bridges-log');
|
|
425
524
|
let firstBridge = true;
|
|
@@ -433,7 +532,18 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
433
532
|
const payload = event.detail && event.detail.payload;
|
|
434
533
|
const time = new Date().toLocaleTimeString();
|
|
435
534
|
|
|
436
|
-
|
|
535
|
+
// Check if it is a state update event
|
|
536
|
+
if (tag === '__elmssr_state__' && event.detail.__elmssr_origin) {
|
|
537
|
+
const marker = findMarkerByOrigin(event.detail.__elmssr_origin);
|
|
538
|
+
if (marker) {
|
|
539
|
+
islandActiveStates.set(marker, payload);
|
|
540
|
+
const activeTab = panel.querySelector('.debug-tab.active')?.getAttribute('data-tab');
|
|
541
|
+
if (activeTab === 'islands') {
|
|
542
|
+
scanIslands();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const li = document.createElement('li');
|
|
437
547
|
li.className = 'debug-item';
|
|
438
548
|
li.innerHTML = \`
|
|
439
549
|
<div class="debug-item-meta" style="flex: 1;">
|
|
@@ -472,35 +582,11 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
472
582
|
</div>
|
|
473
583
|
\`;
|
|
474
584
|
|
|
585
|
+
const dbPane = panel.querySelector('#pane-database');
|
|
586
|
+
dbPane.innerHTML = getDbHtml(newData.effects);
|
|
587
|
+
|
|
475
588
|
const effectsPane = panel.querySelector('#pane-effects');
|
|
476
|
-
|
|
477
|
-
if (newData.effects && newData.effects.length > 0) {
|
|
478
|
-
newEffectsHtml = newData.effects.map(eff => {
|
|
479
|
-
const isSql = ['query', 'queryOne', 'execute'].includes(eff.kind);
|
|
480
|
-
const sqlSnippet = isSql ? \`<pre class="debug-code">\${eff.payload.sql}</pre>\` : '';
|
|
481
|
-
const paramsSnippet = isSql && eff.payload.params && eff.payload.params.length > 0 ?
|
|
482
|
-
\`<div><strong>Params:</strong> \${JSON.stringify(eff.payload.params)}</div>\` : '';
|
|
483
|
-
const outcome = eff.ok ? '<span class="status-ok">SUCCESS</span>' : '<span class="status-error">FAILED</span>';
|
|
484
|
-
const detailError = eff.error ? \`<div class="status-error">\${eff.error}</div>\` : '';
|
|
485
|
-
return \`
|
|
486
|
-
<li class="debug-item">
|
|
487
|
-
<div class="debug-item-meta" style="flex: 1;">
|
|
488
|
-
<div style="display: flex; gap: 8px; align-items: center;">
|
|
489
|
-
<span class="debug-badge badge-primary">\${eff.kind}</span>
|
|
490
|
-
\${outcome}
|
|
491
|
-
<span style="color: #9ca3af; font-size: 0.85em;">\${eff.durationMs.toFixed(1)}ms</span>
|
|
492
|
-
</div>
|
|
493
|
-
\${sqlSnippet}
|
|
494
|
-
\${paramsSnippet}
|
|
495
|
-
\${detailError}
|
|
496
|
-
</div>
|
|
497
|
-
</li>
|
|
498
|
-
\`;
|
|
499
|
-
}).join('');
|
|
500
|
-
effectsPane.innerHTML = \`<ul class="debug-list">\${newEffectsHtml}</ul>\`;
|
|
501
|
-
} else {
|
|
502
|
-
effectsPane.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 20px;">No server-side effects executed for this request.</div>';
|
|
503
|
-
}
|
|
589
|
+
effectsPane.innerHTML = getEffectsHtml(newData.effects);
|
|
504
590
|
|
|
505
591
|
const sessionPane = panel.querySelector('#pane-session');
|
|
506
592
|
sessionPane.innerHTML = \`
|
|
@@ -512,6 +598,8 @@ export const injectDebugger = (html: string, data: unknown): string => {
|
|
|
512
598
|
</div>
|
|
513
599
|
\`;
|
|
514
600
|
|
|
601
|
+
setupMutationObservers(); // Observe any newly mounted islands
|
|
602
|
+
|
|
515
603
|
const activeTab = panel.querySelector('.debug-tab.active').getAttribute('data-tab');
|
|
516
604
|
if (activeTab === 'islands') {
|
|
517
605
|
scanIslands();
|
package/src/http.ts
CHANGED
package/src/request-handler.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { RequestSession, SessionStore } from "./types";
|
|
|
6
6
|
|
|
7
7
|
export interface SessionMiddlewareOptions {
|
|
8
8
|
/** HMAC-SHA256 secret used to sign the session cookie. Must not leak. */
|
|
9
|
-
secret: string;
|
|
9
|
+
secret: string | ((env: any) => string);
|
|
10
10
|
/** Where to persist sessions. Use `memorySessionStore()` in dev, `cacheStore(...)` in prod. */
|
|
11
11
|
store: SessionStore;
|
|
12
12
|
/** Cookie name. Default `"session"`. */
|
|
@@ -87,9 +87,18 @@ export const sessionMiddleware = (options: SessionMiddlewareOptions): Middleware
|
|
|
87
87
|
sameSite: options.sameSite ?? "lax" as const
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
const resolveSecret = (env: any): string => {
|
|
91
|
+
if (typeof options.secret === "function") {
|
|
92
|
+
return options.secret(env);
|
|
93
|
+
}
|
|
94
|
+
return options.secret;
|
|
95
|
+
};
|
|
96
|
+
|
|
90
97
|
return async (context, next) => {
|
|
98
|
+
const env = context.env ?? {};
|
|
99
|
+
const secret = resolveSecret(env);
|
|
91
100
|
const cookieHeader = context.request.headers.get("cookie");
|
|
92
|
-
const sessionId = await readSignedCookie(cookieHeader, cookieName,
|
|
101
|
+
const sessionId = await readSignedCookie(cookieHeader, cookieName, secret);
|
|
93
102
|
|
|
94
103
|
let session: RequestSession;
|
|
95
104
|
if (sessionId) {
|
|
@@ -125,7 +134,7 @@ export const sessionMiddleware = (options: SessionMiddlewareOptions): Middleware
|
|
|
125
134
|
csrf: session.csrf,
|
|
126
135
|
expiresAt: Date.now() + maxAge * 1000
|
|
127
136
|
});
|
|
128
|
-
const signed = await signValue(
|
|
137
|
+
const signed = await signValue(secret, session.id);
|
|
129
138
|
return appendSetCookie(response, buildSetCookie(cookieName, signed, maxAge, cookieOptions));
|
|
130
139
|
}
|
|
131
140
|
|