Search code examples
javascriptmeteorrequire

Hide secret server code called from Meteor Methods


I'm trying to wrap my head around how to hide secret server code from a client in Meteor Methods. The docs seem to imply that the following is a general pattern of how it's done. https://guide.meteor.com/structure.html#using-require

(Note that, as per the docs, I'm using require instead of import, since import cannot be used in a conditional block.)

First, here's a Method defined in a file called methods.js and imported both on the client and the server:

/* methods.js */

let ServerCode
if (Meteor.isServer) {
  ServerCode = require('/imports/server-code.js')
}

Meteor.Methods({
  'myMethodName': function() {
    // ... common code
    if (Meteor.isServer) {
      ServerCode.secretFunction()
    }
    // ... common code
  }
})

Second, here's the secret server code in /imports/server-code.js which I am trying not to send to the client:

/* server-code.js */

class ServerCode {
  secretFunction() {
    console.log('TOP SECRET!!!!')
  }
}

const ServerCodeSingleton = new ServerCode()
module.exports = ServerCodeSingleton

But when I examine the source sent over to the client browser, I'm still seeing my secret server code being sent to the client:

Screenshot of the server code being sent to the client

Even when I do a production build, I can still search and find that 'TOP SECRET!!' string. I feel like I'm being too naive in my understanding of how require works, but the Meteor docs make it seem so simple. So what is the correct way to hide secret code that is called from Meteor Methods?


Solution

  • I finally figured this out I think.

    The short version is, ignore what it says here; I believe it's incorrect or at least misleading:

    https://guide.meteor.com/structure.html#using-require

    And follow what it says here instead:

    https://guide.meteor.com/security.html#secret-code

    A longer explanation is: In a server-only file, import the secret code and assign it to a global variable. Then, in a common file, use isServer (or !isSimulation) to conditionally refer to that global variable.

    So my original example might be re-written like this:

    /*   /imports/methods.js   */
    
    // Note: no conditional use of require this time
    
    Meteor.Methods({
      'myMethodName': function() {
        // ... common code
        if (Meteor.isServer) {
          ServerCode.secret() // <-- Defined as a global outside of this file!
        }
        // ... common code
      }
    })
    

    And so the secret code file might look like this:

    /*   /imports/server-code.js   */
    
    class _ServerCode {
      secret() {
        console.log("Shhhhhh, I'm secret()!")
      }
    }
    // Here's the global variable:
    SecretCode = new _SecretCode()
    

    And then in a server-only file it might look like this:

    /*   /server/server-main.js   */
    
    import '/imports/secret-code' // <-- declare the global
    import '/imports/methods' // <-- use the global in here
    

    And then in a client-only file it might look like so:

    /*   /client/client-main.js   */ 
    
    import '/imports/methods'
    
    //...
    
    Meteor.call('myMethodName')
    

    Now the client and server can both execute some of the same exact code in the Method body (DRY), while some secret code can be server-only and won't get sent to the client as well. It's a little annoying to have to resort to using a global variable, but perhaps that's the cleanest option until a fancier version of import comes along that supports built-in lazy-loading of modules.