LCOV - code coverage report
Current view: top level - dashql-app/src/oauth - oauth_redirect_utils.ts Coverage Total Hit
Test: DashQL 915828b Lines: 95.8 % 48 46
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type { OAuthState } from './oauth_types.js';
       2              : import { AppFlavor, FLAVORS } from '../config/flavors.js';
       3              : import { BASE64URL_CODEC } from '../utils/base64.js';
       4              : 
       5            1 : const PUBLIC_OAUTH_ORIGINS = new Set(['https://dashql.app', 'https://stable.dashql.app', 'https://hyperdb.sh']);
       6            1 : const DEVELOPMENT_OAUTH_HOSTS = new Set(['localhost', '127.0.0.1']);
       7              : 
       8            1 : export function isAllowedOAuthCallbackUrl(callbackUrl: string, currentUrl: URL): boolean {
       9            1 :     let target: URL;
      10            1 :     try {
      11            1 :         target = new URL(callbackUrl);
      12            1 :     } catch {
      13            1 :         return false;
      14            1 :     }
      15            1 :     if (target.pathname !== '/oauth.html' || target.search || target.username || target.password || target.hash) {
      16            1 :         return false;
      17              :     }
      18            1 :     if (target.origin === currentUrl.origin) {
      19            1 :         return true;
      20              :     }
      21            1 :     if (PUBLIC_OAUTH_ORIGINS.has(target.origin)) {
      22            1 :         return true;
      23              :     }
      24            1 :     return (target.protocol === 'http:' || target.protocol === 'https:')
      25            1 :         && DEVELOPMENT_OAUTH_HOSTS.has(target.hostname);
      26            1 : }
      27              : 
      28            1 : export function validateOAuthRedirectState(state: OAuthState, now = Date.now()): void {
      29            1 :     if (!state || typeof state !== 'object') {
      30            0 :         throw new Error('OAuth state is missing');
      31              :     }
      32            1 :     if (typeof state.flowId !== 'string' || state.flowId.length === 0) {
      33            1 :         throw new Error('OAuth state has no flow ID');
      34              :     }
      35            1 :     if (state.flowVariant !== 'WEB_OPENER_FLOW' && state.flowVariant !== 'NATIVE_LINK_FLOW') {
      36            1 :         throw new Error('OAuth state has an invalid flow variant');
      37              :     }
      38            1 :     const provider = state.salesforceProvider;
      39            1 :     if (!provider
      40            1 :         || typeof provider.instanceUrl !== 'string'
      41            1 :         || typeof provider.appConsumerKey !== 'string'
      42            1 :         || typeof provider.expiresAt !== 'number') {
      43            0 :         throw new Error('OAuth state has an invalid Salesforce provider');
      44              :     }
      45            1 :     if (provider.expiresAt <= now) {
      46            1 :         throw new Error('OAuth state has expired');
      47              :     }
      48            1 : }
      49              : 
      50              : /// Decode the base64url-encoded OAuth state of a redirect, it is validated separately
      51            1 : export function decodeOAuthState(encoded: string): OAuthState {
      52            1 :     const buffer = BASE64URL_CODEC.decode(encoded);
      53            1 :     return JSON.parse(new TextDecoder().decode(buffer)) as OAuthState;
      54            1 : }
      55              : 
      56              : /// Get the flavor carried in the encoded OAuth state, null if it carries none or an unknown one.
      57              : /// The state is not integrity-protected, the flavor only brands the redirect page.
      58            1 : export function getOAuthStateFlavor(encoded: string | null): AppFlavor | null {
      59            1 :     if (!encoded) {
      60            1 :         return null;
      61              :     }
      62            1 :     try {
      63            1 :         const flavor = decodeOAuthState(encoded)?.flavor;
      64            1 :         return typeof flavor === 'string' ? FLAVORS.get(flavor) ?? null : null;
      65            1 :     } catch {
      66            1 :         return null;
      67            1 :     }
      68            1 : }
        

Generated by: LCOV version 2.0-1