openziti / ziti-sdk-nodejs

An SDK for embedding zero trust into Node.JS applications and web servers to improve security.
Apache License 2.0
72 stars 3 forks source link

ESM with Webpack not running #88

Open nzedler opened 2 months ago

nzedler commented 2 months ago

Hi together,

we're having trouble getting ziti-sdk-nodejs working without modifying the ziti-sdk-nodejs library code under Nextjs (React). We built and call the following test function using the ESM-client-side example code server-side:

'use server';

import ziti from '@openziti/ziti-sdk-nodejs';

export async function TestZiti() {
    // Somehow provide path to identity file, e.g. via env var
    const zitiIdentityFile = process.env.ZITI_IDENTITY_FILE ?? './test_identity.json';
    // Authenticate ourselves onto the Ziti network
    await ziti.init(zitiIdentityFile).catch((err) => { /* probably exit */ });

    const on_resp_data = (obj) => {
        console.log(`response is: ${obj.body.toString('utf8')}`);
    };

    // Perform an HTTP GET request to a dark OpenZiti web service
    ziti.httpRequest(
        'myDarkWebService',            // OpenZiti Service name or HTTP origin part of the URL
        undefined,                     // schemeHostPort parm is mutually-exclusive with serviceName parm
        'GET',
        '/',                           // path part of the URL including query params
        ['Accept: application/json'], // headers
        undefined,                     // optional on_req cb 
        undefined,                     // optional on_req_data cb
        on_resp_data                   // optional on_resp_data cb
    );
}

In order to get it working, we needed to add the dependency node-loader and the following Webpack config in the next.config.js. What we did is copying the ziti_sdk_nodejs.node-file to the build directory to make it available (since Nextjs performs build optimization). Additionally we hat to define the node-loader, that is able to load the binary:

webpack: (config, { isServer, nextRuntime }) => {
    if (isServer && nextRuntime === "nodejs") {
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = path.dirname(__filename);
      config.plugins.push(
        new CopyPlugin({
          patterns: [
            {
              from: path.resolve(__dirname, 'node_modules/@openziti/ziti-sdk-nodejs/build/Release/ziti_sdk_nodejs.node'),
              to: path.resolve(__dirname, '.next/server/vendor-chunks//_next/ziti_sdk_nodejs.node'),
            },
          ],
        })
      );
      config.externals = [
        ...config.externals,
        "_http_server"
      ];
      config.module.rules.push({
        test: /\.node$/,
        loader: "node-loader",
        options: {
          name: "ziti_sdk_nodejs.node",
        },
      });
    }
    return config;
  }

Running this example, we get the following error message:

⨯ ./node_modules/.pnpm/@mapbox+node-pre-gyp@1.0.11/node_modules/@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> <!doctype html>
| <html>
| <head>

Import trace for requested module:
./node_modules/.pnpm/@mapbox+node-pre-gyp@1.0.11/node_modules/@mapbox/node-pre-gyp/lib/util/nw-pre-gyp/index.html
./node_modules/.pnpm/@mapbox+node-pre-gyp@1.0.11/node_modules/@mapbox/node-pre-gyp/lib/ sync ^\.\/.*$
./node_modules/.pnpm/@mapbox+node-pre-gyp@1.0.11/node_modules/@mapbox/node-pre-gyp/lib/node-pre-gyp.js
./node_modules/.pnpm/@openziti+ziti-sdk-nodejs@0.17.0/node_modules/@openziti/ziti-sdk-nodejs/lib/ziti.js
./src/app/test.tsx
./src/app/page.tsx

This is because in @openziti/ziti-sdk-nodejs/lib/ziti.js line 32 to 44, always the else-statement is used. typeof require.context == 'function' is never true, because require is undefined:

if (typeof require.context == 'function') {

  importAll( require.context("../build/", true, /\.node$/) );

} else {

    const binary = require('@mapbox/node-pre-gyp');
    const path = require('path')
    const binding_path = binary.find(path.resolve(path.join(__dirname,'../package.json')), {debug: false});

    binding = require(binding_path);

}

However, if we modify the if-statement to always true, require.context("../build/", true, /\.node$/) works and loads the binary despite typeof require.context == 'function' being false. This works flawlessly. 🎉

// if (typeof require.context == 'function') {
if (true) {

  importAll( require.context("../build/", true, /\.node$/) );

} else {

    const binary = require('@mapbox/node-pre-gyp');
    const path = require('path')
    const binding_path = binary.find(path.resolve(path.join(__dirname,'../package.json')), {debug: false});

    binding = require(binding_path);

}

We don't want to modify the package source in production. Are there any possibilities to get it running without modifying the package source?

nzedler commented 2 months ago

The ziti.js else-statement can NEVER work in environments with Webpack, see the official answer https://github.com/mapbox/node-pre-gyp/issues/308#issuecomment-413556240 to the question if node-pre-gyp can support Webpack.

It seems to me that we have to modify the if-statement to allow Webpack environments (or somehow fulfill the if-statement when running import ziti from '@openziti/ziti-sdk-nodejs', but I have no clue how)