@tetrascience-npm/tetrascience-react-ui 0.7.0-beta.130.1 → 0.7.0-beta.131.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("@tetrascience-npm/ts-connectors-sdk"),c=60,s=1e3,l=5*c*s;class i{baseUrlOverride;connectorId;orgSlug;tokenCache;tokenRefreshThresholdMs;tdpClient;constructor(e={}){this.baseUrlOverride=e.baseUrl,this.connectorId=process.env.CONNECTOR_ID,this.orgSlug=process.env.ORG_SLUG,this.tokenCache=new Map,this.tokenRefreshThresholdMs=e.tokenRefreshThresholdMs||l,this.tdpClient=null}getBaseUrl(){const e=this.baseUrlOverride||process.env.TDP_ENDPOINT;if(!e)throw new Error("TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.");return e}decodeJwtPayload(e){try{const t=e.split(".");if(t.length!==3)return console.warn("Invalid JWT token format"),null;const n=t[1].replace(/-/g,"+").replace(/_/g,"/"),o=n.padEnd(n.length+(4-n.length%4)%4,"=");return JSON.parse(Buffer.from(o,"base64").toString("utf-8"))}catch(t){return console.warn("Error decoding JWT token:",t),null}}isPayloadExpiringSoon(e){if(!e.exp)return console.warn("JWT token has no expiration claim"),!0;const t=e.exp*s,r=Date.now()+this.tokenRefreshThresholdMs;return t<=r}getValidUserJwt(e){const t=this.tokenCache.get(e);return t&&!this.isPayloadExpiringSoon(t.payload)?t.token:(t&&this.tokenCache.delete(e),null)}async getTdpClient(){if(this.tdpClient!==null)return this.tdpClient;if(!this.connectorId||!this.orgSlug)throw new Error("Missing required configuration: CONNECTOR_ID or ORG_SLUG");const e=this.getBaseUrl();try{const t=new a.TDPClient({tdpEndpoint:e,connectorId:this.connectorId,orgSlug:this.orgSlug,artifactType:"data-app"});return await t.init(),this.tdpClient=t,this.tdpClient}catch(t){throw console.error("Failed to initialize TDP client:",t),t}}async getJwtFromTokenRefInternal(e){const t=await this.getTdpClient();try{const r=await t.getValues([e]);if(r&&r.length>0&&r[0]?.jwt){const n=r[0].jwt,o=this.decodeJwtPayload(n);return o&&this.tokenCache.set(e,{token:n,payload:o}),n}console.error(`No JWT found for key '${e}' in connector store`)}catch(r){console.error("Error retrieving JWT token:",r)}return null}async getJwtFromTokenRef(e){if(!e||!this.orgSlug)return console.warn("Missing required parameters for JWT token retrieval"),null;const t=this.getValidUserJwt(e);return t||this.getJwtFromTokenRefInternal(e)}async getUserToken(e){const t=e["ts-auth-token"]||process.env.TS_AUTH_TOKEN;if(t)return t;const r=e["ts-token-ref"];if(r&&this.connectorId){const n=await this.getJwtFromTokenRef(r);if(n)return n;console.warn("Failed to resolve ts-token-ref to JWT token")}else r&&console.error("Connector ID not configured");return console.warn("No valid authentication token found"),null}async getTokenFromExpressRequest(e){return this.getUserToken(e.cookies||{})}clearCache(){this.tokenCache.clear()}}const h=new i;exports.JwtTokenManager=i;exports.jwtManager=h;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("@tetrascience-npm/ts-connectors-sdk"),l="ts-auth-token",h=60,a=1e3,d=5*h*a;class i{baseUrlOverride;connectorId;orgSlug;tokenCache;tokenRefreshThresholdMs;tdpClient;constructor(e={}){this.baseUrlOverride=e.baseUrl,this.connectorId=process.env.CONNECTOR_ID,this.orgSlug=process.env.ORG_SLUG,this.tokenCache=new Map,this.tokenRefreshThresholdMs=e.tokenRefreshThresholdMs||d,this.tdpClient=null}getBaseUrl(){const e=this.baseUrlOverride||process.env.TDP_ENDPOINT;if(!e)throw new Error("TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.");return e}decodeJwtPayload(e){try{const t=e.split(".");if(t.length!==3)return console.warn("Invalid JWT token format"),null;const n=t[1].replace(/-/g,"+").replace(/_/g,"/"),o=n.padEnd(n.length+(4-n.length%4)%4,"=");return JSON.parse(Buffer.from(o,"base64").toString("utf-8"))}catch(t){return console.warn("Error decoding JWT token:",t),null}}isPayloadExpiringSoon(e){if(!e.exp)return console.warn("JWT token has no expiration claim"),!0;const t=e.exp*a,r=Date.now()+this.tokenRefreshThresholdMs;return t<=r}getValidUserJwt(e){const t=this.tokenCache.get(e);return t&&!this.isPayloadExpiringSoon(t.payload)?t.token:(t&&this.tokenCache.delete(e),null)}async getTdpClient(){if(this.tdpClient!==null)return this.tdpClient;if(!this.connectorId||!this.orgSlug)throw new Error("Missing required configuration: CONNECTOR_ID or ORG_SLUG");const e=this.getBaseUrl();try{const t=new c.TDPClient({tdpEndpoint:e,connectorId:this.connectorId,orgSlug:this.orgSlug,artifactType:"data-app"});return await t.init(),this.tdpClient=t,this.tdpClient}catch(t){throw console.error("Failed to initialize TDP client:",t),t}}async getJwtFromTokenRefInternal(e){const t=await this.getTdpClient();try{const r=await t.getValues([e]);if(r&&r.length>0&&r[0]?.jwt){const n=r[0].jwt,o=this.decodeJwtPayload(n);return o&&this.tokenCache.set(e,{token:n,payload:o}),n}console.error(`No JWT found for key '${e}' in connector store`)}catch(r){console.error("Error retrieving JWT token:",r)}return null}async getJwtFromTokenRef(e){if(!e||!this.orgSlug)return console.warn("Missing required parameters for JWT token retrieval"),null;const t=this.getValidUserJwt(e);return t||this.getJwtFromTokenRefInternal(e)}getHeaderAuthToken(e){const t=e?.[l];return(Array.isArray(t)?t[0]:t)||null}async getUserToken(e,t){const r=this.getHeaderAuthToken(t);if(r)return r;const n=e["ts-auth-token"]||process.env.TS_AUTH_TOKEN;if(n)return n;const o=e["ts-token-ref"];if(o&&this.connectorId){const s=await this.getJwtFromTokenRef(o);if(s)return s;console.warn("Failed to resolve ts-token-ref to JWT token")}else o&&console.error("Connector ID not configured");return console.warn("No valid authentication token found"),null}async getTokenFromExpressRequest(e){return this.getUserToken(e.cookies||{},e.headers)}clearCache(){this.tokenCache.clear()}}const u=new i;exports.JwtTokenManager=i;exports.jwtManager=u;
2
2
  //# sourceMappingURL=JwtTokenManager.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"JwtTokenManager.cjs","sources":["../../../src/server/auth/JwtTokenManager.ts"],"sourcesContent":["/**\n * JWT Token Manager for TetraScience Data Apps\n *\n * Handles authentication token retrieval from Express request cookies,\n * supporting both direct JWT tokens (ts-auth-token) and token references\n * (ts-token-ref) that are resolved via the connector K/V store.\n *\n * Note: This manager does not perform cryptographic verification of JWT signatures.\n * Signature verification is the responsibility of the consuming application or\n * the upstream authentication layer. The JWT payload is decoded only to read\n * expiration times for cache invalidation.\n */\n\nimport { TDPClient } from \"@tetrascience-npm/ts-connectors-sdk\";\n\n/** Configuration options for JwtTokenManager */\nexport interface JwtTokenManagerConfig {\n baseUrl?: string;\n tokenRefreshThresholdMs?: number;\n}\n\n/** Cookie dictionary type - matches Express req.cookies */\nexport interface CookieDict {\n [key: string]: string;\n}\n\n/** Express-like request interface (works with Express, Koa, etc.) */\nexport interface ExpressRequestLike {\n cookies?: CookieDict;\n}\n\n/** JWT payload structure */\ninterface JwtPayload {\n exp?: number;\n iat?: number;\n [key: string]: unknown;\n}\n\n/** Cached token entry storing both raw token and parsed payload */\ninterface CachedTokenEntry {\n token: string;\n payload: JwtPayload;\n}\n\n/** Number of seconds in a minute */\nconst SECONDS_PER_MINUTE = 60;\n/** Milliseconds per second */\nconst MS_PER_SECOND = 1000;\nconst DEFAULT_TOKEN_REFRESH_THRESHOLD_MS = 5 * SECONDS_PER_MINUTE * MS_PER_SECOND; // 5 minutes\n\n/**\n * Manages JWT token retrieval from request cookies.\n * Supports both ts-auth-token (direct JWT) and ts-token-ref (resolved via connector store).\n */\nexport class JwtTokenManager {\n private baseUrlOverride: string | undefined;\n private connectorId: string | undefined;\n private orgSlug: string | undefined;\n private tokenCache: Map<string, CachedTokenEntry>;\n private tokenRefreshThresholdMs: number;\n private tdpClient: TDPClient | null;\n\n constructor(config: JwtTokenManagerConfig = {}) {\n this.baseUrlOverride = config.baseUrl;\n this.connectorId = process.env.CONNECTOR_ID;\n this.orgSlug = process.env.ORG_SLUG;\n this.tokenCache = new Map();\n this.tokenRefreshThresholdMs = config.tokenRefreshThresholdMs || DEFAULT_TOKEN_REFRESH_THRESHOLD_MS;\n this.tdpClient = null;\n }\n\n /**\n * Get the base URL for TDP API calls.\n * Throws an error if not configured (either via config.baseUrl or TDP_ENDPOINT env var).\n */\n private getBaseUrl(): string {\n const baseUrl = this.baseUrlOverride || process.env.TDP_ENDPOINT;\n if (!baseUrl) {\n throw new Error(\"TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.\");\n }\n return baseUrl;\n }\n\n /**\n * Decode JWT payload without verifying signature.\n * Used only for reading expiration times for cache invalidation.\n * Signature verification is NOT performed here.\n */\n private decodeJwtPayload(token: string): JwtPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n console.warn(\"Invalid JWT token format\");\n return null;\n }\n\n const payload = parts[1];\n const base64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n\n // Use Buffer.from for Node.js compatibility (atob not available in all runtimes)\n return JSON.parse(Buffer.from(paddedBase64, \"base64\").toString(\"utf-8\"));\n } catch (error) {\n console.warn(\"Error decoding JWT token:\", error);\n return null;\n }\n }\n\n /** Check if payload is expiring within the refresh threshold */\n private isPayloadExpiringSoon(payload: JwtPayload): boolean {\n if (!payload.exp) {\n console.warn(\"JWT token has no expiration claim\");\n return true;\n }\n\n const expiryTimeMs = payload.exp * MS_PER_SECOND;\n const refreshTimeMs = Date.now() + this.tokenRefreshThresholdMs;\n return expiryTimeMs <= refreshTimeMs;\n }\n\n /** Get valid cached token if not expiring */\n private getValidUserJwt(tokenRef: string): string | null {\n const cached = this.tokenCache.get(tokenRef);\n if (cached && !this.isPayloadExpiringSoon(cached.payload)) {\n return cached.token;\n }\n // Clean up expired entry to prevent unbounded cache growth\n if (cached) {\n // TODO: If data apps become high-traffic, consider enhancing with\n // an LRU cache (e.g., lru-cache package) to purge stale entries automatically\n this.tokenCache.delete(tokenRef);\n }\n return null;\n }\n\n /** Initialize or get TDP client */\n private async getTdpClient(): Promise<TDPClient> {\n if (this.tdpClient !== null) {\n return this.tdpClient;\n }\n\n if (!this.connectorId || !this.orgSlug) {\n throw new Error(\"Missing required configuration: CONNECTOR_ID or ORG_SLUG\");\n }\n\n // getBaseUrl() throws if TDP_ENDPOINT not configured - let it propagate\n const baseUrl = this.getBaseUrl();\n\n try {\n const client = new TDPClient({\n tdpEndpoint: baseUrl,\n connectorId: this.connectorId,\n orgSlug: this.orgSlug,\n artifactType: \"data-app\",\n });\n\n await client.init();\n // Only assign after successful initialization to allow retry on failure\n this.tdpClient = client;\n return this.tdpClient;\n } catch (error) {\n // Log init errors but rethrow so callers know something went wrong\n console.error(\"Failed to initialize TDP client:\", error);\n throw error;\n }\n }\n\n /** Retrieve JWT from connector K/V store using getValues (calls getConnectorData internally) */\n private async getJwtFromTokenRefInternal(tokenRef: string): Promise<string | null> {\n // getTdpClient() throws on config errors - let those propagate\n const tdpClient = await this.getTdpClient();\n\n try {\n // getValues returns array of values for the given keys\n // Each value is expected to have a jwt property: { jwt: \"...\" }\n const values = await tdpClient.getValues([tokenRef]);\n\n if (values && values.length > 0 && values[0]?.jwt) {\n const jwtToken = values[0].jwt;\n // Parse and cache both the raw token and its payload to avoid repeated decoding\n const payload = this.decodeJwtPayload(jwtToken);\n if (payload) {\n this.tokenCache.set(tokenRef, { token: jwtToken, payload });\n }\n return jwtToken;\n }\n\n console.error(`No JWT found for key '${tokenRef}' in connector store`);\n } catch (error) {\n console.error(\"Error retrieving JWT token:\", error);\n }\n\n return null;\n }\n\n /** Resolve ts-token-ref to full JWT token (with caching) */\n async getJwtFromTokenRef(tokenRef: string): Promise<string | null> {\n if (!tokenRef || !this.orgSlug) {\n console.warn(\"Missing required parameters for JWT token retrieval\");\n return null;\n }\n\n const cachedUserToken = this.getValidUserJwt(tokenRef);\n if (cachedUserToken) {\n return cachedUserToken;\n }\n\n return this.getJwtFromTokenRefInternal(tokenRef);\n }\n\n /** Get token from cookies (ts-auth-token or resolved ts-token-ref) */\n async getUserToken(cookies: CookieDict): Promise<string | null> {\n // Prefer ts-auth-token or TS_AUTH_TOKEN env var (latter is for local dev testing)\n const authToken = cookies[\"ts-auth-token\"] || process.env.TS_AUTH_TOKEN;\n if (authToken) {\n return authToken;\n }\n\n // Try to resolve ts-token-ref\n const tokenRef = cookies[\"ts-token-ref\"];\n if (tokenRef && this.connectorId) {\n const jwtToken = await this.getJwtFromTokenRef(tokenRef);\n if (jwtToken) {\n return jwtToken;\n }\n console.warn(\"Failed to resolve ts-token-ref to JWT token\");\n } else if (tokenRef) {\n console.error(\"Connector ID not configured\");\n }\n\n console.warn(\"No valid authentication token found\");\n return null;\n }\n\n /**\n * Get user token from an Express request object.\n * This is the primary method for Express middleware integration.\n *\n * @example\n * ```typescript\n * import { jwtManager } from '@tetrascience-npm/tetrascience-react-ui/server';\n *\n * app.use(async (req, res, next) => {\n * const token = await jwtManager.getTokenFromExpressRequest(req);\n * req.tdpAuth = { token, orgSlug: process.env.ORG_SLUG };\n * next();\n * });\n * ```\n */\n async getTokenFromExpressRequest(req: ExpressRequestLike): Promise<string | null> {\n // Handle missing cookies gracefully (e.g., if cookie-parser middleware not installed)\n return this.getUserToken(req.cookies || {});\n }\n\n /** Clear the token cache */\n clearCache(): void {\n this.tokenCache.clear();\n }\n}\n\n/**\n * Global singleton instance.\n *\n * Note: This instance is created when the module is imported. Configuration\n * that depends on environment variables (CONNECTOR_ID, ORG_SLUG, TDP_ENDPOINT)\n * is read at module load time. Ensure these environment\n * variables are set before importing this module.\n *\n * If you need different configuration at runtime, create a new JwtTokenManager\n * instance instead of using this singleton.\n */\nexport const jwtManager = new JwtTokenManager();\n"],"names":["SECONDS_PER_MINUTE","MS_PER_SECOND","DEFAULT_TOKEN_REFRESH_THRESHOLD_MS","JwtTokenManager","config","baseUrl","token","parts","base64","paddedBase64","error","payload","expiryTimeMs","refreshTimeMs","tokenRef","cached","client","TDPClient","tdpClient","values","jwtToken","cachedUserToken","cookies","authToken","req","jwtManager"],"mappings":"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"}
1
+ {"version":3,"file":"JwtTokenManager.cjs","sources":["../../../src/server/auth/JwtTokenManager.ts"],"sourcesContent":["/**\n * JWT Token Manager for TetraScience Data Apps\n *\n * Handles authentication token retrieval from Express request cookies,\n * supporting both direct JWT tokens (ts-auth-token) and token references\n * (ts-token-ref) that are resolved via the connector K/V store.\n *\n * Note: This manager does not perform cryptographic verification of JWT signatures.\n * Signature verification is the responsibility of the consuming application or\n * the upstream authentication layer. The JWT payload is decoded only to read\n * expiration times for cache invalidation.\n */\n\nimport { TDPClient } from \"@tetrascience-npm/ts-connectors-sdk\";\n\n/** Configuration options for JwtTokenManager */\nexport interface JwtTokenManagerConfig {\n baseUrl?: string;\n tokenRefreshThresholdMs?: number;\n}\n\n/** Cookie dictionary type - matches Express req.cookies */\nexport interface CookieDict {\n [key: string]: string;\n}\n\n/** Header dictionary type - matches Express req.headers (keys are lowercased) */\nexport interface HeaderDict {\n [key: string]: string | string[] | undefined;\n}\n\n/** Express-like request interface (works with Express, Koa, etc.) */\nexport interface ExpressRequestLike {\n cookies?: CookieDict;\n headers?: HeaderDict;\n}\n\n/** Request header carrying the resolved JWT injected by the data-apps proxy. */\nconst AUTH_TOKEN_HEADER = \"ts-auth-token\";\n\n/** JWT payload structure */\ninterface JwtPayload {\n exp?: number;\n iat?: number;\n [key: string]: unknown;\n}\n\n/** Cached token entry storing both raw token and parsed payload */\ninterface CachedTokenEntry {\n token: string;\n payload: JwtPayload;\n}\n\n/** Number of seconds in a minute */\nconst SECONDS_PER_MINUTE = 60;\n/** Milliseconds per second */\nconst MS_PER_SECOND = 1000;\nconst DEFAULT_TOKEN_REFRESH_THRESHOLD_MS = 5 * SECONDS_PER_MINUTE * MS_PER_SECOND; // 5 minutes\n\n/**\n * Manages JWT token retrieval from request cookies.\n * Supports both ts-auth-token (direct JWT) and ts-token-ref (resolved via connector store).\n */\nexport class JwtTokenManager {\n private baseUrlOverride: string | undefined;\n private connectorId: string | undefined;\n private orgSlug: string | undefined;\n private tokenCache: Map<string, CachedTokenEntry>;\n private tokenRefreshThresholdMs: number;\n private tdpClient: TDPClient | null;\n\n constructor(config: JwtTokenManagerConfig = {}) {\n this.baseUrlOverride = config.baseUrl;\n this.connectorId = process.env.CONNECTOR_ID;\n this.orgSlug = process.env.ORG_SLUG;\n this.tokenCache = new Map();\n this.tokenRefreshThresholdMs = config.tokenRefreshThresholdMs || DEFAULT_TOKEN_REFRESH_THRESHOLD_MS;\n this.tdpClient = null;\n }\n\n /**\n * Get the base URL for TDP API calls.\n * Throws an error if not configured (either via config.baseUrl or TDP_ENDPOINT env var).\n */\n private getBaseUrl(): string {\n const baseUrl = this.baseUrlOverride || process.env.TDP_ENDPOINT;\n if (!baseUrl) {\n throw new Error(\"TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.\");\n }\n return baseUrl;\n }\n\n /**\n * Decode JWT payload without verifying signature.\n * Used only for reading expiration times for cache invalidation.\n * Signature verification is NOT performed here.\n */\n private decodeJwtPayload(token: string): JwtPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n console.warn(\"Invalid JWT token format\");\n return null;\n }\n\n const payload = parts[1];\n const base64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n\n // Use Buffer.from for Node.js compatibility (atob not available in all runtimes)\n return JSON.parse(Buffer.from(paddedBase64, \"base64\").toString(\"utf-8\"));\n } catch (error) {\n console.warn(\"Error decoding JWT token:\", error);\n return null;\n }\n }\n\n /** Check if payload is expiring within the refresh threshold */\n private isPayloadExpiringSoon(payload: JwtPayload): boolean {\n if (!payload.exp) {\n console.warn(\"JWT token has no expiration claim\");\n return true;\n }\n\n const expiryTimeMs = payload.exp * MS_PER_SECOND;\n const refreshTimeMs = Date.now() + this.tokenRefreshThresholdMs;\n return expiryTimeMs <= refreshTimeMs;\n }\n\n /** Get valid cached token if not expiring */\n private getValidUserJwt(tokenRef: string): string | null {\n const cached = this.tokenCache.get(tokenRef);\n if (cached && !this.isPayloadExpiringSoon(cached.payload)) {\n return cached.token;\n }\n // Clean up expired entry to prevent unbounded cache growth\n if (cached) {\n // TODO: If data apps become high-traffic, consider enhancing with\n // an LRU cache (e.g., lru-cache package) to purge stale entries automatically\n this.tokenCache.delete(tokenRef);\n }\n return null;\n }\n\n /** Initialize or get TDP client */\n private async getTdpClient(): Promise<TDPClient> {\n if (this.tdpClient !== null) {\n return this.tdpClient;\n }\n\n if (!this.connectorId || !this.orgSlug) {\n throw new Error(\"Missing required configuration: CONNECTOR_ID or ORG_SLUG\");\n }\n\n // getBaseUrl() throws if TDP_ENDPOINT not configured - let it propagate\n const baseUrl = this.getBaseUrl();\n\n try {\n const client = new TDPClient({\n tdpEndpoint: baseUrl,\n connectorId: this.connectorId,\n orgSlug: this.orgSlug,\n artifactType: \"data-app\",\n });\n\n await client.init();\n // Only assign after successful initialization to allow retry on failure\n this.tdpClient = client;\n return this.tdpClient;\n } catch (error) {\n // Log init errors but rethrow so callers know something went wrong\n console.error(\"Failed to initialize TDP client:\", error);\n throw error;\n }\n }\n\n /** Retrieve JWT from connector K/V store using getValues (calls getConnectorData internally) */\n private async getJwtFromTokenRefInternal(tokenRef: string): Promise<string | null> {\n // getTdpClient() throws on config errors - let those propagate\n const tdpClient = await this.getTdpClient();\n\n try {\n // getValues returns array of values for the given keys\n // Each value is expected to have a jwt property: { jwt: \"...\" }\n const values = await tdpClient.getValues([tokenRef]);\n\n if (values && values.length > 0 && values[0]?.jwt) {\n const jwtToken = values[0].jwt;\n // Parse and cache both the raw token and its payload to avoid repeated decoding\n const payload = this.decodeJwtPayload(jwtToken);\n if (payload) {\n this.tokenCache.set(tokenRef, { token: jwtToken, payload });\n }\n return jwtToken;\n }\n\n console.error(`No JWT found for key '${tokenRef}' in connector store`);\n } catch (error) {\n console.error(\"Error retrieving JWT token:\", error);\n }\n\n return null;\n }\n\n /** Resolve ts-token-ref to full JWT token (with caching) */\n async getJwtFromTokenRef(tokenRef: string): Promise<string | null> {\n if (!tokenRef || !this.orgSlug) {\n console.warn(\"Missing required parameters for JWT token retrieval\");\n return null;\n }\n\n const cachedUserToken = this.getValidUserJwt(tokenRef);\n if (cachedUserToken) {\n return cachedUserToken;\n }\n\n return this.getJwtFromTokenRefInternal(tokenRef);\n }\n\n /**\n * Read the ts-auth-token request header, normalizing the array form that\n * Node/Express may produce for a repeated header.\n */\n private getHeaderAuthToken(headers?: HeaderDict): string | null {\n const raw = headers?.[AUTH_TOKEN_HEADER];\n const value = Array.isArray(raw) ? raw[0] : raw;\n return value || null;\n }\n\n /**\n * Get token from a request (proxy-injected header, then cookie, then token ref).\n *\n * Precedence (SW-2352):\n * 1. `ts-auth-token` request header — for large-JWT sessions the data-apps\n * proxy resolves the token reference server-side and injects the real JWT\n * here; the `ts-token-ref` cookie is no longer forwarded to the container.\n * Checking the header first is also what keeps SSO users authenticated.\n * 2. `ts-auth-token` cookie / `TS_AUTH_TOKEN` env var (small-JWT + local dev).\n * 3. `ts-token-ref` cookie → connector K/V resolution (legacy fallback; goes\n * dead in prod once the proxy stops forwarding the cookie, retained for\n * local dev and defense in depth).\n */\n async getUserToken(cookies: CookieDict, headers?: HeaderDict): Promise<string | null> {\n // Prefer the proxy-injected ts-auth-token request header.\n const headerAuthToken = this.getHeaderAuthToken(headers);\n if (headerAuthToken) {\n return headerAuthToken;\n }\n\n // Then ts-auth-token cookie or TS_AUTH_TOKEN env var (latter is for local dev testing)\n const authToken = cookies[\"ts-auth-token\"] || process.env.TS_AUTH_TOKEN;\n if (authToken) {\n return authToken;\n }\n\n // Try to resolve ts-token-ref (legacy fallback).\n // TODO(next major): drop the ts-token-ref → connector-KV resolution entirely.\n // As of SW-2352 the data-apps proxy resolves the reference server-side and\n // injects the JWT via the ts-auth-token header (preferred above), and no\n // longer forwards the ts-token-ref cookie to the container — so this path is\n // dead in production and kept only for local dev / older proxies.\n const tokenRef = cookies[\"ts-token-ref\"];\n if (tokenRef && this.connectorId) {\n const jwtToken = await this.getJwtFromTokenRef(tokenRef);\n if (jwtToken) {\n return jwtToken;\n }\n console.warn(\"Failed to resolve ts-token-ref to JWT token\");\n } else if (tokenRef) {\n console.error(\"Connector ID not configured\");\n }\n\n console.warn(\"No valid authentication token found\");\n return null;\n }\n\n /**\n * Get user token from an Express request object.\n * This is the primary method for Express middleware integration.\n *\n * @example\n * ```typescript\n * import { jwtManager } from '@tetrascience-npm/tetrascience-react-ui/server';\n *\n * app.use(async (req, res, next) => {\n * const token = await jwtManager.getTokenFromExpressRequest(req);\n * req.tdpAuth = { token, orgSlug: process.env.ORG_SLUG };\n * next();\n * });\n * ```\n */\n async getTokenFromExpressRequest(req: ExpressRequestLike): Promise<string | null> {\n // Handle missing cookies gracefully (e.g., if cookie-parser middleware not installed).\n // Headers are checked first (proxy-injected ts-auth-token) — see getUserToken.\n return this.getUserToken(req.cookies || {}, req.headers);\n }\n\n /** Clear the token cache */\n clearCache(): void {\n this.tokenCache.clear();\n }\n}\n\n/**\n * Global singleton instance.\n *\n * Note: This instance is created when the module is imported. Configuration\n * that depends on environment variables (CONNECTOR_ID, ORG_SLUG, TDP_ENDPOINT)\n * is read at module load time. Ensure these environment\n * variables are set before importing this module.\n *\n * If you need different configuration at runtime, create a new JwtTokenManager\n * instance instead of using this singleton.\n */\nexport const jwtManager = new JwtTokenManager();\n"],"names":["AUTH_TOKEN_HEADER","SECONDS_PER_MINUTE","MS_PER_SECOND","DEFAULT_TOKEN_REFRESH_THRESHOLD_MS","JwtTokenManager","config","baseUrl","token","parts","base64","paddedBase64","error","payload","expiryTimeMs","refreshTimeMs","tokenRef","cached","client","TDPClient","tdpClient","values","jwtToken","cachedUserToken","headers","raw","cookies","headerAuthToken","authToken","req","jwtManager"],"mappings":"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"}
@@ -19,9 +19,14 @@ export interface JwtTokenManagerConfig {
19
19
  export interface CookieDict {
20
20
  [key: string]: string;
21
21
  }
22
+ /** Header dictionary type - matches Express req.headers (keys are lowercased) */
23
+ export interface HeaderDict {
24
+ [key: string]: string | string[] | undefined;
25
+ }
22
26
  /** Express-like request interface (works with Express, Koa, etc.) */
23
27
  export interface ExpressRequestLike {
24
28
  cookies?: CookieDict;
29
+ headers?: HeaderDict;
25
30
  }
26
31
  /**
27
32
  * Manages JWT token retrieval from request cookies.
@@ -56,8 +61,25 @@ export declare class JwtTokenManager {
56
61
  private getJwtFromTokenRefInternal;
57
62
  /** Resolve ts-token-ref to full JWT token (with caching) */
58
63
  getJwtFromTokenRef(tokenRef: string): Promise<string | null>;
59
- /** Get token from cookies (ts-auth-token or resolved ts-token-ref) */
60
- getUserToken(cookies: CookieDict): Promise<string | null>;
64
+ /**
65
+ * Read the ts-auth-token request header, normalizing the array form that
66
+ * Node/Express may produce for a repeated header.
67
+ */
68
+ private getHeaderAuthToken;
69
+ /**
70
+ * Get token from a request (proxy-injected header, then cookie, then token ref).
71
+ *
72
+ * Precedence (SW-2352):
73
+ * 1. `ts-auth-token` request header — for large-JWT sessions the data-apps
74
+ * proxy resolves the token reference server-side and injects the real JWT
75
+ * here; the `ts-token-ref` cookie is no longer forwarded to the container.
76
+ * Checking the header first is also what keeps SSO users authenticated.
77
+ * 2. `ts-auth-token` cookie / `TS_AUTH_TOKEN` env var (small-JWT + local dev).
78
+ * 3. `ts-token-ref` cookie → connector K/V resolution (legacy fallback; goes
79
+ * dead in prod once the proxy stops forwarding the cookie, retained for
80
+ * local dev and defense in depth).
81
+ */
82
+ getUserToken(cookies: CookieDict, headers?: HeaderDict): Promise<string | null>;
61
83
  /**
62
84
  * Get user token from an Express request object.
63
85
  * This is the primary method for Express middleware integration.
@@ -1,6 +1,6 @@
1
- import { TDPClient as i } from "@tetrascience-npm/ts-connectors-sdk";
2
- const a = 60, s = 1e3, c = 5 * a * s;
3
- class l {
1
+ import { TDPClient as a } from "@tetrascience-npm/ts-connectors-sdk";
2
+ const c = "ts-auth-token", l = 60, i = 1e3, h = 5 * l * i;
3
+ class d {
4
4
  baseUrlOverride;
5
5
  connectorId;
6
6
  orgSlug;
@@ -8,7 +8,7 @@ class l {
8
8
  tokenRefreshThresholdMs;
9
9
  tdpClient;
10
10
  constructor(e = {}) {
11
- this.baseUrlOverride = e.baseUrl, this.connectorId = process.env.CONNECTOR_ID, this.orgSlug = process.env.ORG_SLUG, this.tokenCache = /* @__PURE__ */ new Map(), this.tokenRefreshThresholdMs = e.tokenRefreshThresholdMs || c, this.tdpClient = null;
11
+ this.baseUrlOverride = e.baseUrl, this.connectorId = process.env.CONNECTOR_ID, this.orgSlug = process.env.ORG_SLUG, this.tokenCache = /* @__PURE__ */ new Map(), this.tokenRefreshThresholdMs = e.tokenRefreshThresholdMs || h, this.tdpClient = null;
12
12
  }
13
13
  /**
14
14
  * Get the base URL for TDP API calls.
@@ -40,7 +40,7 @@ class l {
40
40
  isPayloadExpiringSoon(e) {
41
41
  if (!e.exp)
42
42
  return console.warn("JWT token has no expiration claim"), !0;
43
- const t = e.exp * s, r = Date.now() + this.tokenRefreshThresholdMs;
43
+ const t = e.exp * i, r = Date.now() + this.tokenRefreshThresholdMs;
44
44
  return t <= r;
45
45
  }
46
46
  /** Get valid cached token if not expiring */
@@ -56,7 +56,7 @@ class l {
56
56
  throw new Error("Missing required configuration: CONNECTOR_ID or ORG_SLUG");
57
57
  const e = this.getBaseUrl();
58
58
  try {
59
- const t = new i({
59
+ const t = new a({
60
60
  tdpEndpoint: e,
61
61
  connectorId: this.connectorId,
62
62
  orgSlug: this.orgSlug,
@@ -89,18 +89,41 @@ class l {
89
89
  const t = this.getValidUserJwt(e);
90
90
  return t || this.getJwtFromTokenRefInternal(e);
91
91
  }
92
- /** Get token from cookies (ts-auth-token or resolved ts-token-ref) */
93
- async getUserToken(e) {
94
- const t = e["ts-auth-token"] || process.env.TS_AUTH_TOKEN;
95
- if (t)
96
- return t;
97
- const r = e["ts-token-ref"];
98
- if (r && this.connectorId) {
99
- const n = await this.getJwtFromTokenRef(r);
100
- if (n)
101
- return n;
92
+ /**
93
+ * Read the ts-auth-token request header, normalizing the array form that
94
+ * Node/Express may produce for a repeated header.
95
+ */
96
+ getHeaderAuthToken(e) {
97
+ const t = e?.[c];
98
+ return (Array.isArray(t) ? t[0] : t) || null;
99
+ }
100
+ /**
101
+ * Get token from a request (proxy-injected header, then cookie, then token ref).
102
+ *
103
+ * Precedence (SW-2352):
104
+ * 1. `ts-auth-token` request header — for large-JWT sessions the data-apps
105
+ * proxy resolves the token reference server-side and injects the real JWT
106
+ * here; the `ts-token-ref` cookie is no longer forwarded to the container.
107
+ * Checking the header first is also what keeps SSO users authenticated.
108
+ * 2. `ts-auth-token` cookie / `TS_AUTH_TOKEN` env var (small-JWT + local dev).
109
+ * 3. `ts-token-ref` cookie → connector K/V resolution (legacy fallback; goes
110
+ * dead in prod once the proxy stops forwarding the cookie, retained for
111
+ * local dev and defense in depth).
112
+ */
113
+ async getUserToken(e, t) {
114
+ const r = this.getHeaderAuthToken(t);
115
+ if (r)
116
+ return r;
117
+ const n = e["ts-auth-token"] || process.env.TS_AUTH_TOKEN;
118
+ if (n)
119
+ return n;
120
+ const o = e["ts-token-ref"];
121
+ if (o && this.connectorId) {
122
+ const s = await this.getJwtFromTokenRef(o);
123
+ if (s)
124
+ return s;
102
125
  console.warn("Failed to resolve ts-token-ref to JWT token");
103
- } else r && console.error("Connector ID not configured");
126
+ } else o && console.error("Connector ID not configured");
104
127
  return console.warn("No valid authentication token found"), null;
105
128
  }
106
129
  /**
@@ -119,16 +142,16 @@ class l {
119
142
  * ```
120
143
  */
121
144
  async getTokenFromExpressRequest(e) {
122
- return this.getUserToken(e.cookies || {});
145
+ return this.getUserToken(e.cookies || {}, e.headers);
123
146
  }
124
147
  /** Clear the token cache */
125
148
  clearCache() {
126
149
  this.tokenCache.clear();
127
150
  }
128
151
  }
129
- const u = new l();
152
+ const g = new d();
130
153
  export {
131
- l as JwtTokenManager,
132
- u as jwtManager
154
+ d as JwtTokenManager,
155
+ g as jwtManager
133
156
  };
134
157
  //# sourceMappingURL=JwtTokenManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"JwtTokenManager.js","sources":["../../../src/server/auth/JwtTokenManager.ts"],"sourcesContent":["/**\n * JWT Token Manager for TetraScience Data Apps\n *\n * Handles authentication token retrieval from Express request cookies,\n * supporting both direct JWT tokens (ts-auth-token) and token references\n * (ts-token-ref) that are resolved via the connector K/V store.\n *\n * Note: This manager does not perform cryptographic verification of JWT signatures.\n * Signature verification is the responsibility of the consuming application or\n * the upstream authentication layer. The JWT payload is decoded only to read\n * expiration times for cache invalidation.\n */\n\nimport { TDPClient } from \"@tetrascience-npm/ts-connectors-sdk\";\n\n/** Configuration options for JwtTokenManager */\nexport interface JwtTokenManagerConfig {\n baseUrl?: string;\n tokenRefreshThresholdMs?: number;\n}\n\n/** Cookie dictionary type - matches Express req.cookies */\nexport interface CookieDict {\n [key: string]: string;\n}\n\n/** Express-like request interface (works with Express, Koa, etc.) */\nexport interface ExpressRequestLike {\n cookies?: CookieDict;\n}\n\n/** JWT payload structure */\ninterface JwtPayload {\n exp?: number;\n iat?: number;\n [key: string]: unknown;\n}\n\n/** Cached token entry storing both raw token and parsed payload */\ninterface CachedTokenEntry {\n token: string;\n payload: JwtPayload;\n}\n\n/** Number of seconds in a minute */\nconst SECONDS_PER_MINUTE = 60;\n/** Milliseconds per second */\nconst MS_PER_SECOND = 1000;\nconst DEFAULT_TOKEN_REFRESH_THRESHOLD_MS = 5 * SECONDS_PER_MINUTE * MS_PER_SECOND; // 5 minutes\n\n/**\n * Manages JWT token retrieval from request cookies.\n * Supports both ts-auth-token (direct JWT) and ts-token-ref (resolved via connector store).\n */\nexport class JwtTokenManager {\n private baseUrlOverride: string | undefined;\n private connectorId: string | undefined;\n private orgSlug: string | undefined;\n private tokenCache: Map<string, CachedTokenEntry>;\n private tokenRefreshThresholdMs: number;\n private tdpClient: TDPClient | null;\n\n constructor(config: JwtTokenManagerConfig = {}) {\n this.baseUrlOverride = config.baseUrl;\n this.connectorId = process.env.CONNECTOR_ID;\n this.orgSlug = process.env.ORG_SLUG;\n this.tokenCache = new Map();\n this.tokenRefreshThresholdMs = config.tokenRefreshThresholdMs || DEFAULT_TOKEN_REFRESH_THRESHOLD_MS;\n this.tdpClient = null;\n }\n\n /**\n * Get the base URL for TDP API calls.\n * Throws an error if not configured (either via config.baseUrl or TDP_ENDPOINT env var).\n */\n private getBaseUrl(): string {\n const baseUrl = this.baseUrlOverride || process.env.TDP_ENDPOINT;\n if (!baseUrl) {\n throw new Error(\"TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.\");\n }\n return baseUrl;\n }\n\n /**\n * Decode JWT payload without verifying signature.\n * Used only for reading expiration times for cache invalidation.\n * Signature verification is NOT performed here.\n */\n private decodeJwtPayload(token: string): JwtPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n console.warn(\"Invalid JWT token format\");\n return null;\n }\n\n const payload = parts[1];\n const base64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n\n // Use Buffer.from for Node.js compatibility (atob not available in all runtimes)\n return JSON.parse(Buffer.from(paddedBase64, \"base64\").toString(\"utf-8\"));\n } catch (error) {\n console.warn(\"Error decoding JWT token:\", error);\n return null;\n }\n }\n\n /** Check if payload is expiring within the refresh threshold */\n private isPayloadExpiringSoon(payload: JwtPayload): boolean {\n if (!payload.exp) {\n console.warn(\"JWT token has no expiration claim\");\n return true;\n }\n\n const expiryTimeMs = payload.exp * MS_PER_SECOND;\n const refreshTimeMs = Date.now() + this.tokenRefreshThresholdMs;\n return expiryTimeMs <= refreshTimeMs;\n }\n\n /** Get valid cached token if not expiring */\n private getValidUserJwt(tokenRef: string): string | null {\n const cached = this.tokenCache.get(tokenRef);\n if (cached && !this.isPayloadExpiringSoon(cached.payload)) {\n return cached.token;\n }\n // Clean up expired entry to prevent unbounded cache growth\n if (cached) {\n // TODO: If data apps become high-traffic, consider enhancing with\n // an LRU cache (e.g., lru-cache package) to purge stale entries automatically\n this.tokenCache.delete(tokenRef);\n }\n return null;\n }\n\n /** Initialize or get TDP client */\n private async getTdpClient(): Promise<TDPClient> {\n if (this.tdpClient !== null) {\n return this.tdpClient;\n }\n\n if (!this.connectorId || !this.orgSlug) {\n throw new Error(\"Missing required configuration: CONNECTOR_ID or ORG_SLUG\");\n }\n\n // getBaseUrl() throws if TDP_ENDPOINT not configured - let it propagate\n const baseUrl = this.getBaseUrl();\n\n try {\n const client = new TDPClient({\n tdpEndpoint: baseUrl,\n connectorId: this.connectorId,\n orgSlug: this.orgSlug,\n artifactType: \"data-app\",\n });\n\n await client.init();\n // Only assign after successful initialization to allow retry on failure\n this.tdpClient = client;\n return this.tdpClient;\n } catch (error) {\n // Log init errors but rethrow so callers know something went wrong\n console.error(\"Failed to initialize TDP client:\", error);\n throw error;\n }\n }\n\n /** Retrieve JWT from connector K/V store using getValues (calls getConnectorData internally) */\n private async getJwtFromTokenRefInternal(tokenRef: string): Promise<string | null> {\n // getTdpClient() throws on config errors - let those propagate\n const tdpClient = await this.getTdpClient();\n\n try {\n // getValues returns array of values for the given keys\n // Each value is expected to have a jwt property: { jwt: \"...\" }\n const values = await tdpClient.getValues([tokenRef]);\n\n if (values && values.length > 0 && values[0]?.jwt) {\n const jwtToken = values[0].jwt;\n // Parse and cache both the raw token and its payload to avoid repeated decoding\n const payload = this.decodeJwtPayload(jwtToken);\n if (payload) {\n this.tokenCache.set(tokenRef, { token: jwtToken, payload });\n }\n return jwtToken;\n }\n\n console.error(`No JWT found for key '${tokenRef}' in connector store`);\n } catch (error) {\n console.error(\"Error retrieving JWT token:\", error);\n }\n\n return null;\n }\n\n /** Resolve ts-token-ref to full JWT token (with caching) */\n async getJwtFromTokenRef(tokenRef: string): Promise<string | null> {\n if (!tokenRef || !this.orgSlug) {\n console.warn(\"Missing required parameters for JWT token retrieval\");\n return null;\n }\n\n const cachedUserToken = this.getValidUserJwt(tokenRef);\n if (cachedUserToken) {\n return cachedUserToken;\n }\n\n return this.getJwtFromTokenRefInternal(tokenRef);\n }\n\n /** Get token from cookies (ts-auth-token or resolved ts-token-ref) */\n async getUserToken(cookies: CookieDict): Promise<string | null> {\n // Prefer ts-auth-token or TS_AUTH_TOKEN env var (latter is for local dev testing)\n const authToken = cookies[\"ts-auth-token\"] || process.env.TS_AUTH_TOKEN;\n if (authToken) {\n return authToken;\n }\n\n // Try to resolve ts-token-ref\n const tokenRef = cookies[\"ts-token-ref\"];\n if (tokenRef && this.connectorId) {\n const jwtToken = await this.getJwtFromTokenRef(tokenRef);\n if (jwtToken) {\n return jwtToken;\n }\n console.warn(\"Failed to resolve ts-token-ref to JWT token\");\n } else if (tokenRef) {\n console.error(\"Connector ID not configured\");\n }\n\n console.warn(\"No valid authentication token found\");\n return null;\n }\n\n /**\n * Get user token from an Express request object.\n * This is the primary method for Express middleware integration.\n *\n * @example\n * ```typescript\n * import { jwtManager } from '@tetrascience-npm/tetrascience-react-ui/server';\n *\n * app.use(async (req, res, next) => {\n * const token = await jwtManager.getTokenFromExpressRequest(req);\n * req.tdpAuth = { token, orgSlug: process.env.ORG_SLUG };\n * next();\n * });\n * ```\n */\n async getTokenFromExpressRequest(req: ExpressRequestLike): Promise<string | null> {\n // Handle missing cookies gracefully (e.g., if cookie-parser middleware not installed)\n return this.getUserToken(req.cookies || {});\n }\n\n /** Clear the token cache */\n clearCache(): void {\n this.tokenCache.clear();\n }\n}\n\n/**\n * Global singleton instance.\n *\n * Note: This instance is created when the module is imported. Configuration\n * that depends on environment variables (CONNECTOR_ID, ORG_SLUG, TDP_ENDPOINT)\n * is read at module load time. Ensure these environment\n * variables are set before importing this module.\n *\n * If you need different configuration at runtime, create a new JwtTokenManager\n * instance instead of using this singleton.\n */\nexport const jwtManager = new JwtTokenManager();\n"],"names":["SECONDS_PER_MINUTE","MS_PER_SECOND","DEFAULT_TOKEN_REFRESH_THRESHOLD_MS","JwtTokenManager","config","baseUrl","token","parts","base64","paddedBase64","error","payload","expiryTimeMs","refreshTimeMs","tokenRef","cached","client","TDPClient","tdpClient","values","jwtToken","cachedUserToken","cookies","authToken","req","jwtManager"],"mappings":"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"}
1
+ {"version":3,"file":"JwtTokenManager.js","sources":["../../../src/server/auth/JwtTokenManager.ts"],"sourcesContent":["/**\n * JWT Token Manager for TetraScience Data Apps\n *\n * Handles authentication token retrieval from Express request cookies,\n * supporting both direct JWT tokens (ts-auth-token) and token references\n * (ts-token-ref) that are resolved via the connector K/V store.\n *\n * Note: This manager does not perform cryptographic verification of JWT signatures.\n * Signature verification is the responsibility of the consuming application or\n * the upstream authentication layer. The JWT payload is decoded only to read\n * expiration times for cache invalidation.\n */\n\nimport { TDPClient } from \"@tetrascience-npm/ts-connectors-sdk\";\n\n/** Configuration options for JwtTokenManager */\nexport interface JwtTokenManagerConfig {\n baseUrl?: string;\n tokenRefreshThresholdMs?: number;\n}\n\n/** Cookie dictionary type - matches Express req.cookies */\nexport interface CookieDict {\n [key: string]: string;\n}\n\n/** Header dictionary type - matches Express req.headers (keys are lowercased) */\nexport interface HeaderDict {\n [key: string]: string | string[] | undefined;\n}\n\n/** Express-like request interface (works with Express, Koa, etc.) */\nexport interface ExpressRequestLike {\n cookies?: CookieDict;\n headers?: HeaderDict;\n}\n\n/** Request header carrying the resolved JWT injected by the data-apps proxy. */\nconst AUTH_TOKEN_HEADER = \"ts-auth-token\";\n\n/** JWT payload structure */\ninterface JwtPayload {\n exp?: number;\n iat?: number;\n [key: string]: unknown;\n}\n\n/** Cached token entry storing both raw token and parsed payload */\ninterface CachedTokenEntry {\n token: string;\n payload: JwtPayload;\n}\n\n/** Number of seconds in a minute */\nconst SECONDS_PER_MINUTE = 60;\n/** Milliseconds per second */\nconst MS_PER_SECOND = 1000;\nconst DEFAULT_TOKEN_REFRESH_THRESHOLD_MS = 5 * SECONDS_PER_MINUTE * MS_PER_SECOND; // 5 minutes\n\n/**\n * Manages JWT token retrieval from request cookies.\n * Supports both ts-auth-token (direct JWT) and ts-token-ref (resolved via connector store).\n */\nexport class JwtTokenManager {\n private baseUrlOverride: string | undefined;\n private connectorId: string | undefined;\n private orgSlug: string | undefined;\n private tokenCache: Map<string, CachedTokenEntry>;\n private tokenRefreshThresholdMs: number;\n private tdpClient: TDPClient | null;\n\n constructor(config: JwtTokenManagerConfig = {}) {\n this.baseUrlOverride = config.baseUrl;\n this.connectorId = process.env.CONNECTOR_ID;\n this.orgSlug = process.env.ORG_SLUG;\n this.tokenCache = new Map();\n this.tokenRefreshThresholdMs = config.tokenRefreshThresholdMs || DEFAULT_TOKEN_REFRESH_THRESHOLD_MS;\n this.tdpClient = null;\n }\n\n /**\n * Get the base URL for TDP API calls.\n * Throws an error if not configured (either via config.baseUrl or TDP_ENDPOINT env var).\n */\n private getBaseUrl(): string {\n const baseUrl = this.baseUrlOverride || process.env.TDP_ENDPOINT;\n if (!baseUrl) {\n throw new Error(\"TDP base URL not configured. Set TDP_ENDPOINT environment variable or pass baseUrl in config.\");\n }\n return baseUrl;\n }\n\n /**\n * Decode JWT payload without verifying signature.\n * Used only for reading expiration times for cache invalidation.\n * Signature verification is NOT performed here.\n */\n private decodeJwtPayload(token: string): JwtPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n console.warn(\"Invalid JWT token format\");\n return null;\n }\n\n const payload = parts[1];\n const base64 = payload.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n\n // Use Buffer.from for Node.js compatibility (atob not available in all runtimes)\n return JSON.parse(Buffer.from(paddedBase64, \"base64\").toString(\"utf-8\"));\n } catch (error) {\n console.warn(\"Error decoding JWT token:\", error);\n return null;\n }\n }\n\n /** Check if payload is expiring within the refresh threshold */\n private isPayloadExpiringSoon(payload: JwtPayload): boolean {\n if (!payload.exp) {\n console.warn(\"JWT token has no expiration claim\");\n return true;\n }\n\n const expiryTimeMs = payload.exp * MS_PER_SECOND;\n const refreshTimeMs = Date.now() + this.tokenRefreshThresholdMs;\n return expiryTimeMs <= refreshTimeMs;\n }\n\n /** Get valid cached token if not expiring */\n private getValidUserJwt(tokenRef: string): string | null {\n const cached = this.tokenCache.get(tokenRef);\n if (cached && !this.isPayloadExpiringSoon(cached.payload)) {\n return cached.token;\n }\n // Clean up expired entry to prevent unbounded cache growth\n if (cached) {\n // TODO: If data apps become high-traffic, consider enhancing with\n // an LRU cache (e.g., lru-cache package) to purge stale entries automatically\n this.tokenCache.delete(tokenRef);\n }\n return null;\n }\n\n /** Initialize or get TDP client */\n private async getTdpClient(): Promise<TDPClient> {\n if (this.tdpClient !== null) {\n return this.tdpClient;\n }\n\n if (!this.connectorId || !this.orgSlug) {\n throw new Error(\"Missing required configuration: CONNECTOR_ID or ORG_SLUG\");\n }\n\n // getBaseUrl() throws if TDP_ENDPOINT not configured - let it propagate\n const baseUrl = this.getBaseUrl();\n\n try {\n const client = new TDPClient({\n tdpEndpoint: baseUrl,\n connectorId: this.connectorId,\n orgSlug: this.orgSlug,\n artifactType: \"data-app\",\n });\n\n await client.init();\n // Only assign after successful initialization to allow retry on failure\n this.tdpClient = client;\n return this.tdpClient;\n } catch (error) {\n // Log init errors but rethrow so callers know something went wrong\n console.error(\"Failed to initialize TDP client:\", error);\n throw error;\n }\n }\n\n /** Retrieve JWT from connector K/V store using getValues (calls getConnectorData internally) */\n private async getJwtFromTokenRefInternal(tokenRef: string): Promise<string | null> {\n // getTdpClient() throws on config errors - let those propagate\n const tdpClient = await this.getTdpClient();\n\n try {\n // getValues returns array of values for the given keys\n // Each value is expected to have a jwt property: { jwt: \"...\" }\n const values = await tdpClient.getValues([tokenRef]);\n\n if (values && values.length > 0 && values[0]?.jwt) {\n const jwtToken = values[0].jwt;\n // Parse and cache both the raw token and its payload to avoid repeated decoding\n const payload = this.decodeJwtPayload(jwtToken);\n if (payload) {\n this.tokenCache.set(tokenRef, { token: jwtToken, payload });\n }\n return jwtToken;\n }\n\n console.error(`No JWT found for key '${tokenRef}' in connector store`);\n } catch (error) {\n console.error(\"Error retrieving JWT token:\", error);\n }\n\n return null;\n }\n\n /** Resolve ts-token-ref to full JWT token (with caching) */\n async getJwtFromTokenRef(tokenRef: string): Promise<string | null> {\n if (!tokenRef || !this.orgSlug) {\n console.warn(\"Missing required parameters for JWT token retrieval\");\n return null;\n }\n\n const cachedUserToken = this.getValidUserJwt(tokenRef);\n if (cachedUserToken) {\n return cachedUserToken;\n }\n\n return this.getJwtFromTokenRefInternal(tokenRef);\n }\n\n /**\n * Read the ts-auth-token request header, normalizing the array form that\n * Node/Express may produce for a repeated header.\n */\n private getHeaderAuthToken(headers?: HeaderDict): string | null {\n const raw = headers?.[AUTH_TOKEN_HEADER];\n const value = Array.isArray(raw) ? raw[0] : raw;\n return value || null;\n }\n\n /**\n * Get token from a request (proxy-injected header, then cookie, then token ref).\n *\n * Precedence (SW-2352):\n * 1. `ts-auth-token` request header — for large-JWT sessions the data-apps\n * proxy resolves the token reference server-side and injects the real JWT\n * here; the `ts-token-ref` cookie is no longer forwarded to the container.\n * Checking the header first is also what keeps SSO users authenticated.\n * 2. `ts-auth-token` cookie / `TS_AUTH_TOKEN` env var (small-JWT + local dev).\n * 3. `ts-token-ref` cookie → connector K/V resolution (legacy fallback; goes\n * dead in prod once the proxy stops forwarding the cookie, retained for\n * local dev and defense in depth).\n */\n async getUserToken(cookies: CookieDict, headers?: HeaderDict): Promise<string | null> {\n // Prefer the proxy-injected ts-auth-token request header.\n const headerAuthToken = this.getHeaderAuthToken(headers);\n if (headerAuthToken) {\n return headerAuthToken;\n }\n\n // Then ts-auth-token cookie or TS_AUTH_TOKEN env var (latter is for local dev testing)\n const authToken = cookies[\"ts-auth-token\"] || process.env.TS_AUTH_TOKEN;\n if (authToken) {\n return authToken;\n }\n\n // Try to resolve ts-token-ref (legacy fallback).\n // TODO(next major): drop the ts-token-ref → connector-KV resolution entirely.\n // As of SW-2352 the data-apps proxy resolves the reference server-side and\n // injects the JWT via the ts-auth-token header (preferred above), and no\n // longer forwards the ts-token-ref cookie to the container — so this path is\n // dead in production and kept only for local dev / older proxies.\n const tokenRef = cookies[\"ts-token-ref\"];\n if (tokenRef && this.connectorId) {\n const jwtToken = await this.getJwtFromTokenRef(tokenRef);\n if (jwtToken) {\n return jwtToken;\n }\n console.warn(\"Failed to resolve ts-token-ref to JWT token\");\n } else if (tokenRef) {\n console.error(\"Connector ID not configured\");\n }\n\n console.warn(\"No valid authentication token found\");\n return null;\n }\n\n /**\n * Get user token from an Express request object.\n * This is the primary method for Express middleware integration.\n *\n * @example\n * ```typescript\n * import { jwtManager } from '@tetrascience-npm/tetrascience-react-ui/server';\n *\n * app.use(async (req, res, next) => {\n * const token = await jwtManager.getTokenFromExpressRequest(req);\n * req.tdpAuth = { token, orgSlug: process.env.ORG_SLUG };\n * next();\n * });\n * ```\n */\n async getTokenFromExpressRequest(req: ExpressRequestLike): Promise<string | null> {\n // Handle missing cookies gracefully (e.g., if cookie-parser middleware not installed).\n // Headers are checked first (proxy-injected ts-auth-token) — see getUserToken.\n return this.getUserToken(req.cookies || {}, req.headers);\n }\n\n /** Clear the token cache */\n clearCache(): void {\n this.tokenCache.clear();\n }\n}\n\n/**\n * Global singleton instance.\n *\n * Note: This instance is created when the module is imported. Configuration\n * that depends on environment variables (CONNECTOR_ID, ORG_SLUG, TDP_ENDPOINT)\n * is read at module load time. Ensure these environment\n * variables are set before importing this module.\n *\n * If you need different configuration at runtime, create a new JwtTokenManager\n * instance instead of using this singleton.\n */\nexport const jwtManager = new JwtTokenManager();\n"],"names":["AUTH_TOKEN_HEADER","SECONDS_PER_MINUTE","MS_PER_SECOND","DEFAULT_TOKEN_REFRESH_THRESHOLD_MS","JwtTokenManager","config","baseUrl","token","parts","base64","paddedBase64","error","payload","expiryTimeMs","refreshTimeMs","tokenRef","cached","client","TDPClient","tdpClient","values","jwtToken","cachedUserToken","headers","raw","cookies","headerAuthToken","authToken","req","jwtManager"],"mappings":"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"}
@@ -2,4 +2,4 @@
2
2
  * Authentication utilities for TetraScience data apps
3
3
  */
4
4
  export { JwtTokenManager, jwtManager } from './JwtTokenManager';
5
- export type { JwtTokenManagerConfig, CookieDict, ExpressRequestLike, } from './JwtTokenManager';
5
+ export type { JwtTokenManagerConfig, CookieDict, HeaderDict, ExpressRequestLike, } from './JwtTokenManager';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tetrascience-npm/tetrascience-react-ui",
3
- "version": "0.7.0-beta.130.1",
3
+ "version": "0.7.0-beta.131.1",
4
4
  "description": "TetraScience React UI",
5
5
  "type": "module",
6
6
  "author": "TetraScience",