New package: wildhart:dynamic-import-auth - require auth for lazy loaded code

Hello everyone :waving_hand: @wildhart here (Chris) - I used to be fairly active here pretty much since the start of Meteor and for a good few years, but have been quiet recently. In the background I’ve still been busily maintaining my own Meteor-based SaaS product [1] and apps for private-clients, plus supporting my packages wildhart:jobs and wildhart:partitioner - both recently updated for Meteor 3.

One of those private clients requested that all the UI and front-end logic for their in-house app should be completely private to hide their company IP. The app does quite a bit of data manipulation and Word, Excel and PDF file generation in the frontend, so there is quite a bit of company IP in the front-end code.

While almost all of the UI is lazy-loaded per-route and component, the lazily loaded modules are served by the /__meteor__/dynamic-import/fetch endpoint without any authentication, and Meteor’s own dynamic-versions.js in the main bundle even lists the paths, so it’s fairly easy for a non-logged in user to identify and fetch ALL the front-end code. With AI tools it’s much easier these days for the AI to scrape this code, render the UI and copy the business logic.

Yes, you can move all this code server-side, but that shouldn’t be necessary. A complex, almost spreadsheet-like UI for managing multi-million dollar construction projects requires lots of instant validation and interactive UI built into the front end, and PDF/Word/Excel generation within the browser is much more responsive for the user and keeps blocking, synchronous code off the server. There should be a way to make this code private.

So I wrote a fairly simple package to require cookie auth on the /fetch endpoint: wildhart:dynamic-import-auth

A feature like this has been requested several times:

There are some caveats for making this work - mainly the frontend should never try to fetch code while the user is logged out - but the package provides a front-end helper method to defer route modules and lazy components, e.g. in Vue:

lazy loaded route:

import { deferUntilLoggedIn} from 'meteor/wildhart:dynamic-import-auth';

routes = {
   ...
   {
      path: '/projects',
      component: deferUntilLoggedIn(() => import('/imports/ui/Projects.vue')),
      meta: {auth: true},
   }

lazy loaded component, within the main non-lazy app bar:

components: {
    SyncStatus: deferUntilLoggedIn(() => import('/imports/ui/SyncStatus.vue')),
}

I hope someone finds it useful! Any feedback welcome.

9 Likes

Nice package. Would this work with the rspack integration? app-code import() doesn’t seem to go through /_meteor_/dynamic-import/fetch. rspack writes async chunks to public/build-chunks/[id].[chunkhash].js. But, I could be reading something wrong and not getting the whole picture.

Thanks @matt54, and good catch, this package will NOT protect chunks served with rspack.

Having no experience of rspack myself, this is what Claude told me to write :wink:

:robot: With the rspack integration, app code is bundled by rspack, and its import() calls go through rspack’s own runtime, which loads /build-chunks/[id].[chunkhash].js as ordinary static files (the rspack package serves them through WebAppInternals.staticFilesMiddleware). They never touch /__meteor__/dynamic-import/fetch, so this package doesn’t protect them. Lazy modules inside Atmosphere packages still go through dynamic-import, so those are still covered, but that’s unlikely to be where anyone’s IP lives.

I’ve only used it with the classic bundler so far, and I’ll add a note to the README about rspack.

:robot: Supporting rspack looks doable: the same cookie check could be mounted on /build-chunks/ (same-origin script tags send the HttpOnly cookie). The main catch is caching: those files are served as cacheable static assets, so we’d need to make sure gated chunks aren’t stored by a CDN or shared proxy.

If anyone is using rspack and wants this, feel free to upvote the new issue I created: Support the Meteor rspack integration (`/build-chunks/`) · Issue #1 · wildhart/meteor.dynamic-import-auth · GitHub

1 Like

Great, and thanks for confirming my read. I ran into the same ordering questions building an in-house scheduler for a small business, where the sign-in page and the websocket are both hidden behind a gate on rawConnectHandlers, so I’ve spent some time in webapp’s middleware chain. Two details:

The chunk dir isn’t fixed. rspack uses Meteor.chunksContext or the RSPACK_CHUNKS_CONTEXT env var if set. If METEOR_LOCAL_DIR is set it becomes build-chunks-<basename>. So rather than hardcoding /build-chunks/, resolve it the same way.

The other thing was on caching. I made a bunch of notes a while back from reading rspack 2.2.0 and webapp 2.3.0 and meteor-tool 3.5.2. The chunks land in public/, and Meteor registers public/ files as cacheable: false, so webapp serves them with Cache-Control: public, max-age=0. A shared cache has to revalidate every time, and it hits your check. If you wanted to go further, your rawConnectHandlers hook runs before the static middleware and send only sets Cache-Control when it isn’t already there. So the gist is that setting Cache-Control: private on gated chunks before next() will persist.

2 Likes

It sounds like you’re well placed to do a PR in my package repo @matt54 :wink: Please feel free!

If not I’ll try to get round to it soonish. It’s much easier with AI tools these days to quickly spin up a repro Meteor/rspack project to test with. If I built it, would you use it? There’s no point unless someone actually wants to use this feature…

Honestly, probably not for my app. It’s a single bundle with no import() at all, so there’s no lazy code to gate, and the gate in front already keeps the bundle from anyone without the link.

One thing that might matter for anyone who does use it, separate from rspack: having a valid token doesn’t always mean “should get this code”. Apps with public sign-up, a database shared with another app (my case: a website where anyone can register), or admin only lazy modules would all let the wrong users through. An optional hook like isAllowed(user), maybe even per module, would let apps require a role or a flag instead of just a session.

I’ll see if I can “pull” myself back to the PR world. Some sort of /build-chunks/ support, if I can make the time.

Yeah. Fortunately though, in my case it’s a staff-only website - yes there are admin-only routes, but we can trust the staff - they’re already privy to the company IP, so I was aware of this limitation but happily ignored it for my current use-case!

But role-based protection might be nice for some people, so anyone who wants this - feel free to raise an issue in GH and I’ll look into it.

In this reply @jorgeer wrote:

Ideally we’d have something like import("./some_module", { staffOnly: true }) which placed the import in some restricted area at compile time and checked the logged in user’s isStaff flag when it was requested.

But that puts the {staffOnly: true} guard in the front-end code, so somehow the backend would have to parse all that to know what to allow or not. There might be some child components/packages which are lazily loaded on public routes as well as private routes, so they would have to be allowed. Could get tricky…

1 Like

Nice work, @wildhart, and thanks for sharing this! It’s good to see community packages exploring more of what Meteor’s bundling can support. These contributions bring real application requirements into the discussion and help us understand where the tooling can be more flexible. Your staff-only app is a useful example of why controlling when client code is delivered can matter.

Also, +1 to @matt54’s point about authorization. I noticed you already expose an authorize(userId, req) hook, which gives applications room to require staff membership, a role, or another condition beyond having a valid session. Applying different rules to individual modules or chunks could be an interesting next step, although shared dependencies make that a bigger topic.

For Rspack, the accounts-express middleware you’re already using looks like a useful foundation. The additional work would be protecting Rspack’s chunk requests before static serving, with the same-origin login cookie providing authentication. Happy to help with the Rspack specifics or review a small reproduction and tests, especially around chunk paths, login timing, and caching.

Once the package covers both the Meteor bundler and Rspack, I think it would be a good addition to the Community Packages documentation. Feel free to open a documentation PR in the core repository, meteor/meteor, with a description, setup examples, and the supported scenarios. That would help other developers discover this use case and give the package and your contribution more visibility.

Who knows, some of this could eventually inform support in core too. At the very least, useful community work like this deserves proper recognition and a clear place in the docs.

1 Like