diff --git a/package.json b/package.json
index 4fe962eaf..740a81aee 100644
--- a/package.json
+++ b/package.json
@@ -99,6 +99,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0",
"immer": "^10.0.3",
+ "jose": "^6.0.12",
"lodash.isequal": "^4.5.0",
"react-tooltip": "^5.28.0"
},
diff --git a/src/components/Connect/ConnectProvider.tsx b/src/components/Connect/ConnectProvider.tsx
index d9eba17e6..9f205f422 100644
--- a/src/components/Connect/ConnectProvider.tsx
+++ b/src/components/Connect/ConnectProvider.tsx
@@ -1,5 +1,6 @@
import { useCallback } from "react";
import { ConnectionsProvider } from "context/ConnectionsContextProvider";
+import { InstallationProvider } from "src/headless";
import { useForceUpdate } from "src/hooks/useForceUpdate";
import { Connection } from "src/services/api";
@@ -60,26 +61,35 @@ export function ConnectProvider({
return (
-
-
-
-
-
-
-
+ {/* InstallationProvider is nested in ConnectionsProvider and API service JWT auth */}
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/context/AmpersandContextProvider/AmpersandContextProvider.tsx b/src/context/AmpersandContextProvider/AmpersandContextProvider.tsx
index 6d90bf1d8..081960d53 100644
--- a/src/context/AmpersandContextProvider/AmpersandContextProvider.tsx
+++ b/src/context/AmpersandContextProvider/AmpersandContextProvider.tsx
@@ -10,10 +10,11 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ApiKeyProvider } from "../ApiKeyContextProvider";
import { ErrorStateProvider } from "../ErrorContextProvider";
+import { JwtTokenProvider } from "../JwtTokenContextProvider";
interface AmpersandProviderProps {
options: {
- apiKey: string;
+ apiKey?: string;
/**
* Use `project` instead of `projectId`.
* @deprecated
@@ -24,6 +25,11 @@ interface AmpersandProviderProps {
*/
project?: string;
styles?: object;
+ /**
+ * Callback function to get a JWT token for authorization.
+ * This function should return a Promise that resolves to a JWT token string.
+ */
+ getToken?: (consumerRef: string, groupRef: string) => Promise;
};
children: React.ReactNode;
}
@@ -52,7 +58,7 @@ const queryClient = new QueryClient();
export function AmpersandProvider(props: AmpersandProviderProps) {
const {
- options: { apiKey, projectId, project },
+ options: { apiKey, projectId, project, getToken },
children,
} = props;
const projectIdOrName = project || projectId;
@@ -67,8 +73,16 @@ export function AmpersandProvider(props: AmpersandProviderProps) {
);
}
- if (!apiKey) {
- throw new Error("Cannot use AmpersandProvider without an apiKey.");
+ if (!apiKey && !getToken) {
+ throw new Error(
+ "Cannot use AmpersandProvider without an apiKey or getToken.",
+ );
+ }
+
+ if (apiKey && getToken) {
+ throw new Error(
+ "Cannot use AmpersandProvider with both apiKey and getToken.",
+ );
}
const contextValue: AmpersandContextValue = {
@@ -80,7 +94,9 @@ export function AmpersandProvider(props: AmpersandProviderProps) {
- {children}
+
+ {children}
+
diff --git a/src/context/ApiKeyContextProvider.tsx b/src/context/ApiKeyContextProvider.tsx
index 902c76c59..887f6f8cc 100644
--- a/src/context/ApiKeyContextProvider.tsx
+++ b/src/context/ApiKeyContextProvider.tsx
@@ -6,10 +6,5 @@ export const ApiKeyProvider = ApiKeyContext.Provider;
export const useApiKey = () => {
const apiKey = useContext(ApiKeyContext);
-
- if (apiKey === null) {
- throw new Error("useApiKey must be used within an ApiKeyProvider");
- }
-
return apiKey;
};
diff --git a/src/context/JwtTokenContextProvider.tsx b/src/context/JwtTokenContextProvider.tsx
new file mode 100644
index 000000000..f4f377797
--- /dev/null
+++ b/src/context/JwtTokenContextProvider.tsx
@@ -0,0 +1,213 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import { jwtVerify } from "jose";
+
+interface TokenCacheEntry {
+ token: string;
+ expiresAt: number;
+}
+
+const SESSION_STORAGE_PREFIX = "amp-labs_jwt_";
+
+const DEFAULT_TOKEN_EXPIRATION_TIME = 10 * 60 * 1000; // 10 minutes
+
+const createCacheKey = (consumerRef: string, groupRef: string) =>
+ `${consumerRef}:${groupRef}`;
+
+const getSessionStorageKey = (cacheKey: string) =>
+ `${SESSION_STORAGE_PREFIX}${cacheKey}`;
+
+interface JwtTokenContextValue {
+ getToken?: (consumerRef: string, groupRef: string) => Promise;
+}
+
+const JwtTokenContext = createContext(null);
+
+interface JwtTokenProviderProps {
+ getTokenCallback:
+ | ((consumerRef: string, groupRef: string) => Promise)
+ | null;
+ children: React.ReactNode;
+}
+
+/**
+ * Extract JWT token expiration time
+ */
+const getTokenExpirationTime = async (
+ token: string,
+): Promise => {
+ try {
+ const decoded = await jwtVerify(token, new Uint8Array(0), {
+ algorithms: [], // Skip signature verification
+ });
+ const payload = decoded.payload;
+ return payload.exp && typeof payload.exp === "number"
+ ? payload.exp * 1000 // jwt expiration is in seconds, convert to milliseconds
+ : null;
+ } catch (error) {
+ console.warn("Failed to decode JWT token:", error);
+ return null;
+ }
+};
+
+/**
+ * Simplified JWT token provider with cleaner caching logic
+ */
+export function JwtTokenProvider({
+ getTokenCallback,
+ children,
+}: JwtTokenProviderProps) {
+ const [tokenCache, setTokenCache] = useState