Typescript OSDK V2 - OAuth client

Hi there !

I’m using typescript OSDK V1 in a mobile app.

I’ve noticed that a V2 of the OSDK was out, I tried migrating to it and ran into the follow issue:

The OAuth client (whether it is V1 or V2) looks mainly coded for web (it’s using the web’s local storage to store the token, etc.), so it doesn’t really work for mobile.

To work around this, I setup the OAuth flow natively, and I also handled the token storage and lifecycle natively.

Then, I can just inject this token into the foundry client, something like this:

client.auth.token = {
   accessToken: accessToken,
}

In other words, I natively handle everything token related, and I just inject it into the client, and then I use that client.

Now when trying to migrate to V2, I noticed that in V2, there isn’t a way to set the access token the client is using, so my workaround isn’t valid anymore.

Any insights ? Thanks a lot !

TS2.0 separate client from Auth so you can provide any function which return Promise as an Auth provider.
Since you are constructing the Auth provider, you already have access to it so you can use it wherever you need the token.
See here
and in AuthCallback where we use it.

BTW, Here is an example of an Auth flow for Expo React Native
This does not deal with Token refresh

import { useCallback, useEffect } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { exchangeCodeAsync, makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button,  Linking,  StyleSheet } from 'react-native';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import React from 'react';
import { setFoundryToken } from '@/constants/AsyncStorage';
import { CLIENT_ID, FOUNDRY_URL } from '@/Foundry/osdkConst';
import { useNavigation } from '@react-navigation/native';

WebBrowser.maybeCompleteAuthSession();
const token = EXPO_PUBLIC_FOUNDRY_TOKEN; 

// Endpoint
const discovery = {
  authorizationEndpoint: `${FOUNDRY_URL}/multipass/api/oauth2/authorize`,
  tokenEndpoint: `${FOUNDRY_URL}/multipass/api/oauth2/token`,
};

export const Login: React.FC = ({ navigation : any}) => {
  const navigation = useNavigation();
  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: CLIENT_ID,
      scopes: ["api:read-data", "api:write-data", "api:admin-read"],
      redirectUri: makeRedirectUri({
        path: '/auth/callback',
      }),
      usePKCE: true,
    },
    discovery
  );

  useEffect(() => {
    promptAsync().then((codeResponse) => {
        if (codeResponse.type !== 'success') {
          return;
        }
        exchangeCodeAsync(
          {
            clientId: CLIENT_ID,
            code: codeResponse.params.code,
            redirectUri: makeRedirectUri({ path: '/auth/callback' }),
            extraParams: request?.codeVerifier 
            ? {
              code_verifier: request.codeVerifier
            } : undefined,
          },
          discovery
        ).then( async (tokenResponse) => {
            //console.log("tokenResponse", tokenResponse);
            await setFoundryToken(tokenResponse.accessToken).then(() => {
              navigation.navigate("Home");
            });
          });
        }
      );
  } , [promptAsync, navigation, request]);
  
  return (
      <ThemedView style={styles.loginContainer}>
          <ThemedView style={styles.loginControls}>
              <Button
                  title="Login"
                  onPress={onLogin} />
              <ThemedText type={'subtitle'} style={styles.loginSubTitle}>Powered by Palantir</ThemedText>
          </ThemedView>
      </ThemedView>
);
}


const styles = StyleSheet.create({
  loginContainer: {
      display: "flex",
      flexDirection: "column",
      justifyContent: "center",
      gap: 10,
      padding: 10,
      width: "100%",
      height: "100%",
      backgroundColor: "white",
  },
  loginControls: {
    display: "flex",
    flexDirection: "column",
    justifyContent: "center",
    width: "50%",
    alignSelf: "center",
  },
  loginSubTitle: {
    display: "flex",
    padding: 10,
    alignSelf: "center",
  },
  reactLogo: {
    height: 178,
    width: 290,
    bottom: 0,
    left: 0,
    position: 'absolute',
  },
});

Thanks for your reply !

I tried passing a custom Auth.

The thing is, there are too many “web specific” things hard coded into the client library, which prevents it from working seamlessly on mobile (using expo for example).

The client library is using window.matchMedia, node:crypto, etc.

Of course one could try polyfills when available, but unfortunately, it’s not the only issue.

The client library is also using:

  • import.meta which is not yet handled by Hermes (the official RN JS engine),
  • exports in its package.json which is not yet handled by default by Metro (the bundler), there’s an unsafe option to enable it, but then it might break other things.

I actually have a working version which I am hoping to demo in the comming DevCon next week of Expo application.
Try using this metro.config.js file

// @ts-check
const { mergeConfig } = require('metro-config');
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const defaultConfig = getDefaultConfig(__dirname);

module.exports = mergeConfig(defaultConfig, {
  transformer: {
    ...defaultConfig.transformer,
    babelTransformerPath: require.resolve('react-native-svg-transformer'),
  },
  resolver: {
    unstable_enablePackageExports: true,
    assetExts: (defaultConfig.resolver?.assetExts ?? []).filter(ext => ext !== 'svg'),
    sourceExts: [...(defaultConfig.resolver?.sourceExts ?? []), 'svg'],    

    

    resolveRequest: function (context, moduleName, platform) {
      if (moduleName.includes("@osdk")) {
        context = {
          ...context,
          unstable_conditionNames: ["browser", "require", "import"],
        }
      }
      return context.resolveRequest(context, moduleName, platform);  
    },
  },
});

the code above in combination with this Metro file is what I needed to get it working
LMK if you still have issues

Already configured metro that way.

Did you try running on mobile or are you only testing on web ?

As I said in my previous post, even if you polyfill the missing node libraries, and fix the exports using unstable_enablePackageExports, there are still other issues like the fact that @osdk/client uses import.meta which is not yet handled by Hermes (the official RN JS engine). (So it results in a crash with a “import.meta” is not currently supported)

If you’re only testing on web, it’s gonna probably work as you’d then be using the browser’s JS engine. When running on mobile, the JS engine (Hermes in this case) is shipped alongside the app.

I have tested it on Expo iOS simulator and it works fine.
We don’t have import.meta but we noticed other package do so this is why we are limiting the config in metro to if (moduleName.includes("@osdk")) {
Can you be more specific on where the error is happening and what is the error you are getting?

Managed to fix it !

The error was because of unstable_enablePackageExports: true, it broke a few other packages I have in the project, had to configure metro to resolve them correctly.

Regarding the initial issue (configure the client to use a custom auth), here’s how it works for anyone needing this:

const auth = Object.assign(
  async () => ... // for example: () => someStorage.accessToken || '',
  {}
)

and then:

const client = createClient(FOUNDRY_URL, $ontologyRid, auth)

Thanks again !

PS: I noticed that your expo code seems to be using an older version of expo/RN (explicitly using react-navigation for example instead of expo router), double import from ‘react’ (import React from ‘react’ is no longer necessary), etc. I suggest you update if it’s intended for a demo. Good luck in your demo !