intacct / intacct-sdk-js

Official repository of the Sage Intacct SDK for JavaScript in Node.js
https://developer.intacct.com/tools/sdk-node-js/
Apache License 2.0
24 stars 33 forks source link

Please support client-side #32

Closed wh1t3cAt1k closed 4 years ago

wh1t3cAt1k commented 4 years ago

Current intacct-sdk-js has a dependency on a number of Node.js-specific core modules such as net, tls, fs, and others.

My app intentionally wants to do a fat-client implementation, so it needs to work in the browser. Unfortunately currently it is not possible.

If possible, the current SDK needs to be isomorphic and work in both client-side and server-side JS.

Example errors are attached below:

As we can see, the only directly offending file is ProfileCredentialProvider.js and AttachmentFile.js, otherwise those are in the libraries request-promise-native library.

I am not sure if after fixing these errors no new errors would appear, but I am really looking forward to developing my web app without resorting to server-side logic!

ERROR in ProjectDir/node_modules/@intacct/intacct-sdk/dist/Credentials/ProfileCredentialProvider.js
Module not found: Error: Can't resolve 'fs' in 'ProjectDir\node_modules\@intacct\intacct-sdk\dist\Credentials'
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Credentials/ProfileCredentialProvider.js 20:11-24
...

ERROR in ProjectDir/node_modules/@intacct/intacct-sdk/dist/Functions/Company/AttachmentFile.js
Module not found: Error: Can't resolve 'fs' in 'ProjectDir\node_modules\@intacct\intacct-sdk\dist\Functions\Company'
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Functions/Company/AttachmentFile.js 20:11-24
...

ERROR in ProjectDir/node_modules/request/lib/har.js
Module not found: Error: Can't resolve 'fs' in 'ProjectDir\node_modules\request\lib'
 @ ProjectDir/node_modules/request/lib/har.js 3:9-22
 @ ProjectDir/node_modules/request/request.js
 @ ProjectDir/node_modules/request/index.js
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

ERROR in ProjectDir/node_modules/forever-agent/index.js
Module not found: Error: Can't resolve 'net' in 'ProjectDir\node_modules\forever-agent'
 @ ProjectDir/node_modules/forever-agent/index.js 6:10-24
 @ ProjectDir/node_modules/request/request.js
 @ ProjectDir/node_modules/request/index.js
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

ERROR in ProjectDir/node_modules/tough-cookie/lib/cookie.js
Module not found: Error: Can't resolve 'net' in 'ProjectDir\node_modules\tough-cookie\lib'
 @ ProjectDir/node_modules/tough-cookie/lib/cookie.js 32:10-24
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

ERROR in ProjectDir/node_modules/tunnel-agent/index.js
Module not found: Error: Can't resolve 'net' in 'ProjectDir\node_modules\tunnel-agent'
 @ ProjectDir/node_modules/tunnel-agent/index.js 3:10-24
 @ ProjectDir/node_modules/request/lib/tunnel.js
 @ ProjectDir/node_modules/request/request.js
 @ ProjectDir/node_modules/request/index.js
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

ERROR in ProjectDir/node_modules/forever-agent/index.js
Module not found: Error: Can't resolve 'tls' in 'ProjectDir\node_modules\forever-agent'
 @ ProjectDir/node_modules/forever-agent/index.js 7:10-24
 @ ProjectDir/node_modules/request/request.js
 @ ProjectDir/node_modules/request/index.js
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

ERROR in ProjectDir/node_modules/tunnel-agent/index.js
Module not found: Error: Can't resolve 'tls' in 'ProjectDir\node_modules\tunnel-agent'
 @ ProjectDir/node_modules/tunnel-agent/index.js 4:10-24
 @ ProjectDir/node_modules/request/lib/tunnel.js
 @ ProjectDir/node_modules/request/request.js
 @ ProjectDir/node_modules/request/index.js
 @ ProjectDir/node_modules/request-promise-native/lib/rp.js
 @ ProjectDir/node_modules/@intacct/intacct-sdk/dist/Xml/HttpClientHandler.js
...

i 「wdm」: Failed to compile.

Error from chokidar (C:\): Error: EBUSY: resource busy or locked, lstat 'C:\hiberfil.sys'
Error from chokidar (C:\): Error: EBUSY: resource busy or locked, lstat 'C:\pagefile.sys'
Error from chokidar (C:\): Error: EBUSY: resource busy or locked, lstat 'C:\swapfile.sys'
wh1t3cAt1k commented 4 years ago

I can confirm that mocking the fs and solving the request library problem gets rid of all the webpack errors.

Most of the above problems are caused by dependency on request which is only available server-side.

This can most likely be mitigated by using https://www.npmjs.com/package/cross-fetch instead: this package works across the platform table.

wh1t3cAt1k commented 4 years ago

As a temporary work-around, I was able to monkey-patch the HttpClient to use browser-request instead of just request, where I stripped all headers that the browser considers unsafe, e.g. "User-Agent" and "Accept-Encoding".

I also mocked the fs module to use an in-memory "filesystem" provided by browserify-fs:

resolve: {
    alias: {
        'fs': 'browserify-fs',
        './HttpClientHandler$': path.resolve(__dirname, 'src/.../polyfills/http-client-handler-polyfill')
    },
}

(in webpack.config.js)

import { Response, RequestCallback } from 'request';
import _ from 'lodash';
import { falsyToUndefined } from '../../value-decorators/falsy-to-undefined';

const client: typeof import('request') = require('browser-request');

class HttpClientHandler
{
    private readonly _options: any;

    public constructor(options: any)
    {
        this._options = options;
    }

    public postAsync = async (): Promise<Response> => {
        const promise = new Promise<Response>((resolve, reject) => {
            const requestCallback: RequestCallback = (error: any, response: Response, body: any) => {
                if (falsyToUndefined(error) !== undefined) {
                    reject(error);
                    return;
                }

                if (falsyToUndefined(body) === undefined) {
                    return;
                }

                if (falsyToUndefined(response.headers) === undefined) {
                    response.headers = {
                        "content-type": 'application/xml',
                    }
                }

                resolve(response);
            }

            const browserSafeOptions = _.cloneDeep(this._options);

            this._removeUnsafeHeader(browserSafeOptions, "User-Agent");
            this._removeUnsafeHeader(browserSafeOptions, "Accept-Encoding");

            client(browserSafeOptions, requestCallback);
        });

        const result = await promise;

        return result;
    }

    private readonly _removeUnsafeHeader = (browserSafeOptions: any, headerName: string): void => {
        if (browserSafeOptions?.headers?.[headerName] !== undefined) {
            browserSafeOptions.headers[headerName] = undefined;
        }
    }
}

export default HttpClientHandler;

(polyfill file)

However, I would be glad if this could work out-of-the-box in an isomorphic way.

jimmymcpeter commented 4 years ago

Something like this will not be considered until Sage Intacct Web Services supports public client types via OAuth2. Today, only confidential client types are supported since in a public client like the browser would be exposing the Web Services sender+user passwords.

wh1t3cAt1k commented 4 years ago

@jimmymcpeter sorry, I do not completely understand you. Where would it be exposing the password?

As far as I understand, interacting with the web services API does not require an OAuth authentication, we use session-based authentication.

If the OAuth is an additional option, it can simply be not supported in client-side environments.

I considered forking your SDK to do this manually, but I figured supporting isomorphic JS was not a very strict requirement, so I decided to file it here first.

jimmymcpeter commented 4 years ago

To use web services, you must have a sender ID and sender password. The sender credentials are independent of a user's credentials (session ID or company ID, user ID, and user password).

jimmymcpeter commented 4 years ago

All web services requests have to have a senderid and password, you'd have to hard code this into the code running in the browser. This means it's visible to anyone using your app and goes against the web services developer terms. If you're a marketplace partner this would cause it to also fail the app review.

<request>
    <control>
        <senderid>testsenderid</senderid>
        <password>pass123!</password> <!-- this would be visible -->
        <controlid>unittest</controlid>
        <uniqueid>false</uniqueid>
        <dtdversion>3.0</dtdversion>
        <includewhitespace>false</includewhitespace>
    </control>
    <operation transaction="false">
        <authentication>
            <sessionid>testsession..</sessionid>
        </authentication>
        <content/>
    </operation>
</request>
wh1t3cAt1k commented 4 years ago

@jimmymcpeter I understand.

No, of course it won't be hard-coded into the code in any case.

Consider two options:

  1. The credentials are securely stored on our server and are only passed to the client when it wants to make a request to Intacct.
  2. If this is not secure enough yet, the section can be modified on the fly by the server (working as a proxy) when the client wants to make an API request.

For a number of important reasons, we wish to make our application a fat-client, and there certainly seems to be a way to make it secure...

jimmymcpeter commented 4 years ago

I think the proxy is probably your best choice at the moment. This also gives you control on any user licensing since, if I'm not mistaken, Microsoft has deprecated paid office add-ins in favor of free add-ins+paid SaaS offers.