@antelopejs/dms-frontend 0.5.6 → 0.6.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.
Files changed (71) hide show
  1. package/README.md +376 -18
  2. package/dist/app-configs.d.ts +9 -0
  3. package/dist/app-configs.js +52 -0
  4. package/dist/asset-roots.d.ts +7 -0
  5. package/dist/asset-roots.js +34 -0
  6. package/dist/commands/build-action.js +9 -0
  7. package/dist/commands/dev-action.js +88 -19
  8. package/dist/commands/start-action.js +2 -0
  9. package/dist/common.d.ts +1 -0
  10. package/dist/common.js +1 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +8 -3
  13. package/dist/derived-outputs.d.ts +2 -0
  14. package/dist/derived-outputs.js +38 -31
  15. package/dist/help.js +10 -3
  16. package/dist/layer-watch.js +123 -131
  17. package/dist/layers.js +2 -0
  18. package/dist/manifest-secrets.js +5 -5
  19. package/dist/manifest.js +7 -1
  20. package/dist/materialize.js +21 -1
  21. package/dist/module-set.d.ts +16 -0
  22. package/dist/module-set.js +80 -0
  23. package/dist/server-process.d.ts +1 -0
  24. package/dist/server-process.js +28 -2
  25. package/dist/verify-source-runner.js +12 -0
  26. package/dist/workspace.d.ts +9 -0
  27. package/package.json +2 -1
  28. package/templates/vue/app-runtime.ts +58 -16
  29. package/templates/vue/client-server-guard.mjs +73 -0
  30. package/templates/vue/email-renderer.ts +17 -4
  31. package/templates/vue/email-runtime.ts +1 -1
  32. package/templates/vue/frontend-module.ts +1817 -259
  33. package/templates/vue/globals.d.ts +1 -1
  34. package/templates/vue/icon-hydration.ts +106 -1
  35. package/templates/vue/index.html +0 -1
  36. package/templates/vue/layer-app-config.ts +92 -0
  37. package/templates/vue/main.ts +10 -2
  38. package/templates/vue/package.json +4 -1
  39. package/templates/vue/server/auth/backend.mjs +13 -19
  40. package/templates/vue/server/auth/client-ip.mjs +27 -0
  41. package/templates/vue/server/auth/establish-endpoints.mjs +70 -0
  42. package/templates/vue/server/auth/session.mjs +6 -97
  43. package/templates/vue/server/auth-hooks.mjs +84 -0
  44. package/templates/vue/server/backend-proxies.mjs +110 -0
  45. package/templates/vue/server/dev-private-files.mjs +99 -0
  46. package/templates/vue/server/email.mjs +11 -33
  47. package/templates/vue/server/favicon.mjs +20 -0
  48. package/templates/vue/server/inertia.mjs +48 -3
  49. package/templates/vue/server/listen-host.mjs +19 -0
  50. package/templates/vue/server/module-event.mjs +265 -0
  51. package/templates/vue/server/module-routes.mjs +513 -0
  52. package/templates/vue/server/module-server.mjs +225 -0
  53. package/templates/vue/server/module-testing.mjs +65 -0
  54. package/templates/vue/server/page-props.mjs +151 -0
  55. package/templates/vue/server/path-patterns.mjs +38 -0
  56. package/templates/vue/server/public-endpoints.mjs +69 -11
  57. package/templates/vue/server/request-routing.mjs +47 -0
  58. package/templates/vue/server/route-rules.mjs +129 -0
  59. package/templates/vue/server-module.ts +294 -0
  60. package/templates/vue/server.mjs +177 -161
  61. package/templates/vue/ssr-renderer.ts +60 -19
  62. package/templates/vue/tsconfig.json +2 -1
  63. package/templates/vue/ui-app-config.ts +18 -6
  64. package/templates/vue/ui-inertia-page.mjs +37 -0
  65. package/templates/vue/vite.config.ts +31 -10
  66. package/templates/vue/vite.email.config.ts +1 -1
  67. package/templates/vue/vite.server.config.ts +57 -0
  68. package/templates/vue/server/auth/oauth.mjs +0 -233
  69. package/templates/vue/server/auth/routes.mjs +0 -444
  70. package/templates/vue/server/render-token.mjs +0 -81
  71. package/templates/vue/server/tester.mjs +0 -227
@@ -10,12 +10,8 @@ import {
10
10
  RequestBodyError,
11
11
  UpstreamError,
12
12
  } from "./server/auth/backend.mjs";
13
+ import { forwardedHeaders } from "./server/auth/client-ip.mjs";
13
14
  import { proxyAccess, refuseCrossOrigin } from "./server/public-endpoints.mjs";
14
- import {
15
- handleAuth,
16
- publicSession,
17
- refreshSession,
18
- } from "./server/auth/routes.mjs";
19
15
  import { readSession } from "./server/auth/session.mjs";
20
16
  import {
21
17
  BackendResponseError,
@@ -37,14 +33,24 @@ import {
37
33
  writeViteError,
38
34
  } from "./server/vite-errors.mjs";
39
35
  import { documentStyleTags } from "./server/dev-styles.mjs";
40
- import { handleEmailRender, watchEmailBundle } from "./server/email.mjs";
36
+ import { watchEmailBundle } from "./server/email.mjs";
37
+ import { withFaviconPlaceholder } from "./server/favicon.mjs";
38
+ import { DEV_FS_DENY, denyPrivateFiles } from "./server/dev-private-files.mjs";
39
+ import { ownerLabel, requestOwner } from "./server/request-routing.mjs";
40
+ import { routingReport } from "./server/backend-proxies.mjs";
41
+ import { routeRuleFor, ruleHeaders } from "./server/route-rules.mjs";
42
+ import {
43
+ handleModuleRoute,
44
+ loadServerModules,
45
+ } from "./server/module-server.mjs";
41
46
  import { HOMEPAGE } from "./server/homepage.mjs";
47
+ import { loadPageProps } from "./server/page-props.mjs";
42
48
  import { htmlTag } from "./server/html-tag.mjs";
49
+ import { listenHost } from "./server/listen-host.mjs";
43
50
  import * as icons from "./server/icons.mjs";
44
51
  import * as requestFailure from "./server/request-failure.mjs";
45
52
  import { writeProxyHeaders } from "./server/proxy-headers.mjs";
46
53
  import { backendUrl, refuseForeignTarget } from "./server/request-target.mjs";
47
- import { handleTester } from "./server/tester.mjs";
48
54
  import {
49
55
  createInertiaPage,
50
56
  htmlHeaders,
@@ -64,27 +70,8 @@ const MIME_TYPES = {
64
70
  ".js": "text/javascript",
65
71
  ".svg": "image/svg+xml",
66
72
  };
67
- const PROXY_PREFIXES = ["/api/", "/dms/"];
68
73
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
69
- const AUTH_SERVER_ROUTES = [
70
- ["GET", /^\/api\/_auth\/session\/?$/],
71
- ["POST", /^\/api\/_auth\/session\/?$/],
72
- ["DELETE", /^\/api\/_auth\/session\/?$/],
73
- [
74
- "POST",
75
- /^\/auth\/(?:login|signup|verify-2fa|establish|request-2fa-email|switch-account|switch-tenant|validate-account|remove-account)\/?$/,
76
- ],
77
- ["POST", /^\/auth\/oauth\/handoff\/?$/],
78
- ["GET", /^\/auth\/oauth\/[^/]+\/(?:start|callback)\/?$/],
79
- ];
80
- const HTML_RENDER_ROUTE = "/api/html/render";
81
- const TESTER_ROUTE = /^\/api\/_dms\/tester\/?$/;
82
- const LOCAL_ROUTES = [
83
- [(pathname) => TESTER_ROUTE.test(pathname), handleTester],
84
- [icons.isIconRequest, icons.handleIcons],
85
- ];
86
- const AUTH_PAGE = "/auth";
87
- const ONBOARDING_PAGE = "/onboarding";
74
+ const LOCAL_ROUTES = [[icons.isIconRequest, icons.handleIcons]];
88
75
  const SOURCE_TEMPLATE_PATH = join(PROJECT_ROOT, "index.html");
89
76
  const BUILT_SSR_RENDERER_PATH = join(PROJECT_ROOT, "dist/ssr/ssr-renderer.js");
90
77
  // The SSR bundle keeps vue-i18n external, so Node loads its esm-bundler build,
@@ -102,13 +89,17 @@ async function developmentServer() {
102
89
  if (vite) return vite;
103
90
  vitePromise ??= import("vite")
104
91
  .then(({ createServer: createViteServer }) => {
105
- const serverOptions = { middlewareMode: true };
92
+ const serverOptions = {
93
+ middlewareMode: true,
94
+ // The generated secrets and the modules' server code, never served.
95
+ fs: { deny: DEV_FS_DENY },
96
+ };
106
97
  if (frontendHttpServer)
107
98
  serverOptions.hmr = { server: frontendHttpServer };
108
99
  return createViteServer({
109
100
  server: serverOptions,
110
101
  appType: "custom",
111
- plugins: [captureViteErrors()],
102
+ plugins: [denyPrivateFiles(), captureViteErrors()],
112
103
  });
113
104
  })
114
105
  .then((server) => {
@@ -122,17 +113,28 @@ async function developmentServer() {
122
113
  return vitePromise;
123
114
  }
124
115
 
125
- function backendHeaders(request, publicEndpoint) {
126
- const headers = relayedHeaders(request, [
127
- "accept",
128
- "content-type",
129
- "if-match",
130
- "user-agent",
131
- "x-content-language",
132
- "x-realtime-session",
133
- ...(publicEndpoint?.headers ?? []),
134
- ]);
135
- const session = publicEndpoint ? undefined : readSession(request);
116
+ /**
117
+ * The headers of a backend request: the relayed client headers, the client's
118
+ * Origin and Referer as received, the `X-Forwarded-*` headers (set last, so no
119
+ * caller writes them), and the session's access token unless the request goes
120
+ * anonymously.
121
+ */
122
+ function backendHeaders(request, access = { anonymous: false, headers: [] }) {
123
+ const headers = {
124
+ ...relayedHeaders(request, [
125
+ "accept",
126
+ "content-type",
127
+ "if-match",
128
+ "origin",
129
+ "referer",
130
+ "user-agent",
131
+ "x-content-language",
132
+ "x-realtime-session",
133
+ ...access.headers,
134
+ ]),
135
+ ...forwardedHeaders(request),
136
+ };
137
+ const session = access.anonymous ? undefined : readSession(request);
136
138
  if (session?.accessToken)
137
139
  headers.authorization = `Bearer ${session.accessToken}`;
138
140
  return headers;
@@ -162,37 +164,23 @@ async function backendJson(path, request) {
162
164
  return response.json();
163
165
  }
164
166
 
165
- async function fetchPage(request, pathname) {
166
- const query = new URLSearchParams({ path: pathname });
167
- return backendJson(`/dms/page?${query}`, request);
167
+ /** The session logic of the module that owns sign-in. */
168
+ async function currentAuth() {
169
+ return (await loadServerModules(await developmentServer())).serverAuth;
168
170
  }
169
171
 
170
- async function pageProps(request) {
171
- const pathname = new URL(request.url, "http://frontend.local").pathname;
172
- const session = publicSession(readSession(request));
173
- try {
174
- const page = await fetchPage(request, pathname);
175
- return { path: pathname, page, ...session };
176
- } catch (error) {
177
- if (!(error instanceof BackendResponseError) || error.status !== 404)
178
- throw error;
179
- const renderer = await ssrRenderer(await developmentServer());
180
- if (!renderer.isDmsFrontendPage(pathname)) throw error;
181
- return {
182
- path: pathname,
183
- ...session,
184
- page: {
185
- componentName: pathname,
186
- isFrontendOnly: true,
187
- route: {
188
- fullId: pathname.slice(1).replaceAll("/", "."),
189
- fullSlug: pathname,
190
- hasAccess: pathname.startsWith("/auth/"),
191
- },
192
- layout: { layout: { componentName: "DefaultLayout" } },
193
- },
194
- };
195
- }
172
+ function pageProps(request, auth) {
173
+ return loadPageProps(
174
+ request,
175
+ {
176
+ fetchJson: (path) => backendJson(path, request),
177
+ isFrontendPage: async (pathname) =>
178
+ (await ssrRenderer(await developmentServer())).isDmsFrontendPage(
179
+ pathname,
180
+ ),
181
+ },
182
+ auth,
183
+ );
196
184
  }
197
185
 
198
186
  function redirectToHomepage(request, response, pathname) {
@@ -201,7 +189,7 @@ function redirectToHomepage(request, response, pathname) {
201
189
  return true;
202
190
  }
203
191
 
204
- function redirectSignedOutPrivatePage(request, response, props) {
192
+ function redirectSignedOutPrivatePage(request, response, props, auth) {
205
193
  const route = props.page?.route;
206
194
  if (
207
195
  route?.publicAccess === true ||
@@ -209,27 +197,14 @@ function redirectSignedOutPrivatePage(request, response, props) {
209
197
  readSession(request)
210
198
  )
211
199
  return false;
212
- const location = `${AUTH_PAGE}?redirect=${encodeURIComponent(request.url)}`;
213
- redirectFrontendVisit(request, response, location);
200
+ redirectFrontendVisit(request, response, auth.signInLocation(request.url));
214
201
  return true;
215
202
  }
216
203
 
217
- export function requestOwnership(method, pathname) {
218
- if (PROXY_PREFIXES.some((prefix) => pathname.startsWith(prefix)))
219
- return "server";
220
- if (
221
- AUTH_SERVER_ROUTES.some(
222
- ([routeMethod, pattern]) =>
223
- routeMethod === method && pattern.test(pathname),
224
- )
225
- )
226
- return "server";
227
- return method === "GET" ? "frontend" : "server";
228
- }
229
-
230
- async function proxy(request, response, fallbackOnNotFound = false) {
231
- const { publicEndpoint, isRefused } = proxyAccess(request, SAFE_METHODS);
232
- if (isRefused) {
204
+ /** Sends a request to the backend through the proxy it reached. */
205
+ async function proxy(request, response, backendProxy) {
206
+ const access = proxyAccess(request, SAFE_METHODS, backendProxy);
207
+ if (access.isRefused) {
233
208
  refuseCrossOrigin(response);
234
209
  return true;
235
210
  }
@@ -240,18 +215,13 @@ async function proxy(request, response, fallbackOnNotFound = false) {
240
215
  const upstream = await fetch(backendUrl(request.url), {
241
216
  signal: abandoned.signal,
242
217
  method: request.method,
243
- headers: backendHeaders(request, publicEndpoint),
218
+ headers: backendHeaders(request, access),
244
219
  body: ["GET", "HEAD"].includes(request.method) ? undefined : request,
245
220
  duplex: "half",
246
221
  });
247
- if (fallbackOnNotFound && upstream.status === 404) {
248
- await upstream.body?.cancel();
249
- return false;
250
- }
251
222
  writeProxyHeaders(response, upstream);
252
223
  if (!upstream.body || request.method === "HEAD") response.end();
253
224
  else await pipeline(Readable.fromWeb(upstream.body), response);
254
- return true;
255
225
  }
256
226
 
257
227
  function acceptedAsset(pathname, request) {
@@ -298,11 +268,37 @@ async function ssrRenderer(devServer) {
298
268
  return renderer;
299
269
  }
300
270
 
271
+ function clientRenderedShell(page) {
272
+ return {
273
+ body: inertiaAppHtml(page),
274
+ head: { headTags: "", htmlAttrs: "", bodyAttrs: "" },
275
+ overlays: "",
276
+ };
277
+ }
278
+
279
+ async function serverRendered(page, serverFetch, requestCookies, devServer) {
280
+ try {
281
+ const renderer = await ssrRenderer(devServer);
282
+ return await renderer.renderDmsPage(page, serverFetch, requestCookies);
283
+ } catch (error) {
284
+ console.error(
285
+ "DMS SSR render failed; falling back to client rendering",
286
+ error,
287
+ );
288
+ return { ...clientRenderedShell(page), error: { statusCode: 500 } };
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Renders a page document. With `{ ssr: false }` (a route rule), the client
294
+ * renders the page from the shell instead.
295
+ */
301
296
  export async function renderHtml(
302
297
  page,
303
298
  requestUrl,
304
299
  serverFetch,
305
300
  requestCookies,
301
+ options = {},
306
302
  ) {
307
303
  const devServer = await developmentServer();
308
304
  const rawTemplate = devServer
@@ -311,27 +307,17 @@ export async function renderHtml(
311
307
  const template = devServer
312
308
  ? await devServer.transformIndexHtml(requestUrl, rawTemplate)
313
309
  : rawTemplate;
314
- let rendered;
315
- try {
316
- const renderer = await ssrRenderer(devServer);
317
- rendered = await renderer.renderDmsPage(page, serverFetch, requestCookies);
318
- } catch (error) {
319
- console.error(
320
- "DMS SSR render failed; falling back to client rendering",
321
- error,
322
- );
323
- rendered = {
324
- body: inertiaAppHtml(page),
325
- head: { headTags: "", htmlAttrs: "", bodyAttrs: "" },
326
- overlays: "",
327
- error: { statusCode: 500 },
328
- };
329
- }
310
+ const rendered =
311
+ options.ssr === false
312
+ ? clientRenderedShell(page)
313
+ : await serverRendered(page, serverFetch, requestCookies, devServer);
330
314
  if (rendered.redirect)
331
315
  return { html: "", status: 200, redirect: rendered.redirect };
332
316
  const preloads = await documentStyleTags(devServer, page, template);
333
317
  const html = template
334
- .replace("<title>Antelope DMS</title>", rendered.head.headTags)
318
+ .replace("<title>Antelope DMS</title>", () =>
319
+ withFaviconPlaceholder(rendered.head.headTags),
320
+ )
335
321
  .replace(/<html([^>]*)>/, (_, attributes) =>
336
322
  htmlTag(attributes, rendered.head.htmlAttrs),
337
323
  )
@@ -345,23 +331,24 @@ export async function renderHtml(
345
331
  return { html, status: rendered.error?.statusCode ?? 200 };
346
332
  }
347
333
 
348
- function renderRequestHtml(page, request) {
334
+ function renderRequestHtml(page, request, rule = {}) {
349
335
  const fetch = serverComponentFetch(request);
350
- return renderHtml(page, request.url, fetch, request.headers.cookie);
336
+ return renderHtml(page, request.url, fetch, request.headers.cookie, {
337
+ ssr: rule.ssr,
338
+ });
351
339
  }
352
340
 
353
341
  async function writeBackendError(error, request, response) {
342
+ const auth = await currentAuth();
354
343
  const status = Number.isInteger(error?.status) ? error.status : 502;
355
344
  const pathname = new URL(request.url, "http://frontend.local").pathname;
356
345
  if (
357
346
  error instanceof BackendResponseError &&
358
347
  [401, 403].includes(status) &&
359
348
  !readSession(request) &&
360
- !pathname.startsWith(AUTH_PAGE) &&
361
- pathname !== ONBOARDING_PAGE
349
+ !auth.isSignInExempt(pathname)
362
350
  ) {
363
- const location = `${AUTH_PAGE}?redirect=${encodeURIComponent(request.url)}`;
364
- redirectFrontendVisit(request, response, location);
351
+ redirectFrontendVisit(request, response, auth.signInLocation(request.url));
365
352
  return;
366
353
  }
367
354
  const loadRenderer = async () => ssrRenderer(await developmentServer());
@@ -372,7 +359,7 @@ async function writeBackendError(error, request, response) {
372
359
  const props = {
373
360
  path: pathname,
374
361
  page: {},
375
- ...publicSession(readSession(request)),
362
+ ...auth.view(readSession(request)),
376
363
  error: { statusCode: status },
377
364
  };
378
365
  const page = createInertiaPage(request.url, props);
@@ -403,19 +390,45 @@ async function answeredByVite(devServer, request, response) {
403
390
  }
404
391
 
405
392
  /**
406
- * Vite's own URLs (`/@vite/client`, `/@id/…`, `/@fs/…`) and paths with an
407
- * extension name files: modules, stylesheets, fonts, images, built assets.
408
- * The backend's routes outside `/api` and `/dms` are mostly the paths of the
409
- * pages they serve data for, which have none (`/<page>/pagelayout`).
393
+ * Whether the renderer has a file at `pathname`: a development source Vite
394
+ * serves (under the workspace or its `public/`), or a built asset. Only a
395
+ * path with an extension names one.
410
396
  */
411
- function isFilePath(pathname) {
412
- return pathname.startsWith("/@") || extname(pathname) !== "";
397
+ function hasRendererFile(devServer, requested) {
398
+ let pathname;
399
+ try {
400
+ // As Vite and the asset server read it: `%2E` is a dot.
401
+ pathname = decodeURIComponent(requested);
402
+ } catch {
403
+ return false;
404
+ }
405
+ if (extname(pathname) === "") return false;
406
+ const roots = devServer
407
+ ? [devServer.config.root, devServer.config.publicDir].filter(Boolean)
408
+ : [join(PROJECT_ROOT, "dist/client")];
409
+ return roots.some((root) => {
410
+ const file = resolve(root, `.${pathname}`);
411
+ return (
412
+ file.startsWith(`${resolve(root)}${sep}`) &&
413
+ existsSync(file) &&
414
+ statSync(file).isFile()
415
+ );
416
+ });
417
+ }
418
+
419
+ /** Vite's files in development, the build's in production; else a 404. */
420
+ async function serveRendererFile(devServer, request, response, pathname) {
421
+ const answered = devServer
422
+ ? await answeredByVite(devServer, request, response)
423
+ : serveAsset(pathname, request, response);
424
+ if (answered) return;
425
+ response.writeHead(404, { "content-type": "text/plain" });
426
+ response.end("Not found");
413
427
  }
414
428
 
415
- /** Vite's files in development, the build's in production. */
416
- async function answeredWithFile(devServer, request, response, pathname) {
417
- if (!devServer) return serveAsset(pathname, request, response);
418
- return pathname !== "/" && answeredByVite(devServer, request, response);
429
+ function writeNotFound(response) {
430
+ response.writeHead(404, { "content-type": JSON_TYPE });
431
+ response.end(JSON.stringify({ message: "Not found" }));
419
432
  }
420
433
 
421
434
  export async function handleRequest(request, response) {
@@ -423,48 +436,39 @@ export async function handleRequest(request, response) {
423
436
  const pathname = url.pathname;
424
437
  const localRoute = LOCAL_ROUTES.find(([matches]) => matches(pathname));
425
438
  if (localRoute) return localRoute[1](request, response);
426
- if (pathname === "/api/_auth/session")
427
- return handleAuth(request, response, url);
428
- if (request.method === "POST" && pathname === HTML_RENDER_ROUTE)
429
- return handleEmailRender(request, response);
430
- if (
431
- AUTH_SERVER_ROUTES.some(
432
- ([method, pattern]) =>
433
- method === request.method && pattern.test(pathname),
434
- )
435
- )
436
- return handleAuth(request, response, url);
437
- if (requestOwnership(request.method, pathname) === "server")
438
- return proxy(request, response);
439
+ const devServer = await developmentServer();
440
+ const modules = await loadServerModules(devServer);
441
+ const owner = requestOwner(request.method, pathname, modules, (path) =>
442
+ hasRendererFile(devServer, path),
443
+ );
444
+ if (devServer) response.setHeader("x-dms-route-owner", ownerLabel(owner));
445
+ if (owner.kind === "renderer")
446
+ return serveRendererFile(devServer, request, response, pathname);
447
+ if (owner.kind === "module")
448
+ return handleModuleRoute(request, response, pathname, modules);
449
+ if (owner.kind === "backend") return proxy(request, response, owner.proxy);
450
+ if (owner.kind === "not-found") return writeNotFound(response);
439
451
  if (requestFailure.hasMalformedPath(request.url))
440
452
  return requestFailure.writeBadRequest(response);
441
- const devServer = await developmentServer();
442
- const visit = isFrontendVisit(request);
443
- // A file is looked for among the frontend's own before the backend is asked
444
- // for it: asking first cost a backend round trip for each of the hundreds of
445
- // modules a development page loads, and for each built asset. A file the
446
- // frontend does not have, a media file, is still the backend's.
447
- const fileFirst = !visit && isFilePath(pathname);
448
- const serveFile = () =>
449
- answeredWithFile(devServer, request, response, pathname);
450
- if (fileFirst && (await serveFile())) return;
451
- if (!visit && (await proxy(request, response, true))) return;
452
- if (!fileFirst && (await serveFile())) return;
453
453
  if (handleAssetVersionMismatch(request, response)) return;
454
454
  if (redirectToHomepage(request, response, pathname)) return;
455
- const props = await pageProps(request);
456
- if (redirectSignedOutPrivatePage(request, response, props)) return;
457
- const page = createInertiaPage(request.url, props);
455
+ const { serverAuth: auth, rules } = modules;
456
+ const { props, onceProps } = await pageProps(request, auth);
457
+ if (redirectSignedOutPrivatePage(request, response, props, auth)) return;
458
+ const page = createInertiaPage(request.url, props, undefined, onceProps);
459
+ const rule = routeRuleFor(rules, pathname);
460
+ const headersFor = (status) =>
461
+ ruleHeaders(rule, request, props.page?.route, status);
458
462
  if (request.headers[INERTIA_HEADER]) {
459
463
  return writeContent(
460
464
  request,
461
465
  response,
462
466
  200,
463
- inertiaHeaders(page.version),
467
+ { ...inertiaHeaders(page.version), ...headersFor(200) },
464
468
  JSON.stringify(page),
465
469
  );
466
470
  }
467
- const rendered = await renderRequestHtml(page, request);
471
+ const rendered = await renderRequestHtml(page, request, rule);
468
472
  if (rendered.redirect) {
469
473
  redirectFrontendVisit(request, response, rendered.redirect);
470
474
  return;
@@ -473,7 +477,7 @@ export async function handleRequest(request, response) {
473
477
  request,
474
478
  response,
475
479
  rendered.status,
476
- htmlHeaders(),
480
+ { ...htmlHeaders(), ...headersFor(rendered.status) },
477
481
  rendered.html,
478
482
  );
479
483
  }
@@ -488,7 +492,7 @@ async function writeRequestFailure(error, request, response) {
488
492
  !request.dmsSessionRetry
489
493
  ) {
490
494
  request.dmsSessionRetry = true;
491
- if (await refreshSession(request, response)) {
495
+ if (await (await currentAuth()).refresh(request, response)) {
492
496
  await handleRequestSafely(request, response);
493
497
  return;
494
498
  }
@@ -517,7 +521,7 @@ if (
517
521
  handleClientError(error, socket, maxHeaderSize),
518
522
  );
519
523
  const port = Number(process.env.PORT ?? 3001);
520
- const host = process.env.HOST ?? "0.0.0.0";
524
+ const host = listenHost();
521
525
  const onListenError = (error) => void reportListenError(error, host, port);
522
526
  frontendHttpServer.once("error", onListenError);
523
527
  frontendHttpServer.listen(port, host, () => {
@@ -526,6 +530,17 @@ if (
526
530
  });
527
531
  }
528
532
 
533
+ /** Prints who owns which paths, once the server is up. */
534
+ async function reportRouting() {
535
+ try {
536
+ const modules = await loadServerModules(await developmentServer());
537
+ for (const line of routingReport(modules.proxies, modules.routes.length))
538
+ console.log(line);
539
+ } catch (error) {
540
+ console.error("DMS server modules failed to load", error);
541
+ }
542
+ }
543
+
529
544
  /**
530
545
  * Report the server ready. In development Vite would otherwise start with the
531
546
  * first request, so it is started here: "ready" then means the first page is
@@ -540,6 +555,7 @@ async function announceReady() {
540
555
  }
541
556
  const { address, port } = frontendHttpServer.address();
542
557
  await reportReady(address, port);
558
+ await reportRouting();
543
559
  // Only the production build produces the email bundle otherwise. Started
544
560
  // once the server answers, so it never delays the first page.
545
561
  if (process.env.DMS_DEV === "true") watchEmailBundle();